@deployfoundation/foundation-deploy 0.2.14 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-image/Dockerfile +1 -0
- package/dist/bin/app.js +13 -3
- package/dist/bin/foundation-deploy.js +121 -7
- package/dist/bin/release-account.js +2 -2
- package/dist/{chunk-7qn00ny1.js → chunk-9rasrppa.js} +46 -4
- package/dist/{chunk-e9vs85ah.js → chunk-pygj3x43.js} +1 -1
- package/dist/{chunk-83781ggk.js → chunk-rq4dc5ns.js} +550 -12
- package/dist/src/index.js +4 -2
- package/package.json +1 -1
- package/src/artifacts.ts +2 -0
- package/src/deploy/cli.ts +3 -0
- package/src/deploy/deploy.ts +49 -0
- package/src/deploy/pages-domain.ts +106 -0
- package/src/deploy/runtime-secret.ts +8 -0
- package/src/deploy/setup.ts +13 -1
- package/src/deploy/slack-manifest.ts +44 -3
- package/src/index.ts +1 -0
- package/src/stacks/agent-stack.ts +78 -0
- package/src/stacks/pages-edge-function.ts +149 -0
- package/src/stacks/pages-stack.ts +504 -0
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
scheduledJobBindings,
|
|
20
20
|
slackCommandPrefixes,
|
|
21
21
|
webhookJobBindings
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-9rasrppa.js";
|
|
23
23
|
|
|
24
24
|
// src/stacks/agent-stack.ts
|
|
25
25
|
import * as cdk from "aws-cdk-lib";
|
|
@@ -240,6 +240,60 @@ class FoundationAgent extends cdk.Stack {
|
|
|
240
240
|
resources: [secretArnPattern(knockOauthClient), secretArnPattern(knockCredential)]
|
|
241
241
|
}));
|
|
242
242
|
}
|
|
243
|
+
pagesPolicy(id, role, instance, options) {
|
|
244
|
+
if (!provisionsIntegration(instance, "pages"))
|
|
245
|
+
return;
|
|
246
|
+
const stack = cdk.Stack.of(this);
|
|
247
|
+
const table = stack.formatArn({
|
|
248
|
+
service: "dynamodb",
|
|
249
|
+
resource: "table",
|
|
250
|
+
resourceName: "*-PagesTable*"
|
|
251
|
+
});
|
|
252
|
+
const statements = [
|
|
253
|
+
new iam.PolicyStatement({
|
|
254
|
+
sid: "DenyDirectPagesAccess",
|
|
255
|
+
effect: iam.Effect.DENY,
|
|
256
|
+
actions: [
|
|
257
|
+
"s3:*",
|
|
258
|
+
"dynamodb:*",
|
|
259
|
+
"secretsmanager:GetSecretValue",
|
|
260
|
+
"secretsmanager:BatchGetSecretValue",
|
|
261
|
+
"cloudfront-keyvaluestore:*"
|
|
262
|
+
],
|
|
263
|
+
resources: [
|
|
264
|
+
`arn:${stack.partition}:s3:::*pagesbucket*`,
|
|
265
|
+
`arn:${stack.partition}:s3:::*pagesbucket*/*`,
|
|
266
|
+
table,
|
|
267
|
+
`${table}/index/*`,
|
|
268
|
+
stack.formatArn({
|
|
269
|
+
service: "secretsmanager",
|
|
270
|
+
resource: "secret",
|
|
271
|
+
resourceName: `${this.names.secretPagesCookieKey}-??????`,
|
|
272
|
+
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME
|
|
273
|
+
}),
|
|
274
|
+
`arn:${stack.partition}:cloudfront::${stack.account}:key-value-store/*`
|
|
275
|
+
]
|
|
276
|
+
})
|
|
277
|
+
];
|
|
278
|
+
if (options.invoke)
|
|
279
|
+
statements.push(new iam.PolicyStatement({
|
|
280
|
+
sid: "PagesApiInvoke",
|
|
281
|
+
actions: ["lambda:InvokeFunction"],
|
|
282
|
+
resources: [
|
|
283
|
+
stack.formatArn({
|
|
284
|
+
service: "lambda",
|
|
285
|
+
resource: "function",
|
|
286
|
+
resourceName: this.names.pagesApiFunctionName,
|
|
287
|
+
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME
|
|
288
|
+
})
|
|
289
|
+
]
|
|
290
|
+
}));
|
|
291
|
+
new iam.ManagedPolicy(this, id, {
|
|
292
|
+
description: `${instance.displayName} hosted pages: only through the Pages API`,
|
|
293
|
+
roles: [role],
|
|
294
|
+
statements
|
|
295
|
+
});
|
|
296
|
+
}
|
|
243
297
|
denyDirectUpworkApprovalAccess(role, upworkApprovalTable) {
|
|
244
298
|
if (upworkApprovalTable === undefined)
|
|
245
299
|
return;
|
|
@@ -290,6 +344,7 @@ class FoundationAgent extends cdk.Stack {
|
|
|
290
344
|
this.denyDirectKnockSecretAccess(role, props);
|
|
291
345
|
this.denyDirectUpworkApprovalAccess(role, props.upworkApprovalTable);
|
|
292
346
|
this.denyDirectEtradeAccess(role, props);
|
|
347
|
+
this.pagesPolicy("AgentReadOnlyPagesPolicy", role, props.instance, { invoke: false });
|
|
293
348
|
role.addToPolicy(new iam.PolicyStatement({
|
|
294
349
|
sid: "DenyAgentCoreBrowserMetadata",
|
|
295
350
|
effect: iam.Effect.DENY,
|
|
@@ -350,6 +405,7 @@ class FoundationAgent extends cdk.Stack {
|
|
|
350
405
|
this.denyDirectKnockSecretAccess(role, props);
|
|
351
406
|
this.denyDirectUpworkApprovalAccess(role, props.upworkApprovalTable);
|
|
352
407
|
this.denyDirectEtradeAccess(role, props);
|
|
408
|
+
this.pagesPolicy("AgentExecutionPagesPolicy", role, props.instance, { invoke: true });
|
|
353
409
|
const releaseRepositoryArn = releaseImageRepositoryArn(this);
|
|
354
410
|
role.addToPolicy(new iam.PolicyStatement({
|
|
355
411
|
sid: "EcrPull",
|
|
@@ -2679,18 +2735,500 @@ function senderAddressFor(sender) {
|
|
|
2679
2735
|
return address;
|
|
2680
2736
|
}
|
|
2681
2737
|
|
|
2682
|
-
// src/stacks/
|
|
2738
|
+
// src/stacks/pages-stack.ts
|
|
2683
2739
|
import * as cdk7 from "aws-cdk-lib";
|
|
2740
|
+
import * as acm2 from "aws-cdk-lib/aws-certificatemanager";
|
|
2741
|
+
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
|
|
2742
|
+
import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
|
|
2743
|
+
import * as dynamodb3 from "aws-cdk-lib/aws-dynamodb";
|
|
2744
|
+
import * as iam8 from "aws-cdk-lib/aws-iam";
|
|
2745
|
+
import * as lambda3 from "aws-cdk-lib/aws-lambda";
|
|
2746
|
+
import * as route532 from "aws-cdk-lib/aws-route53";
|
|
2747
|
+
import * as route53Targets2 from "aws-cdk-lib/aws-route53-targets";
|
|
2748
|
+
import * as s32 from "aws-cdk-lib/aws-s3";
|
|
2749
|
+
import * as secretsmanager4 from "aws-cdk-lib/aws-secretsmanager";
|
|
2750
|
+
|
|
2751
|
+
// src/stacks/pages-edge-function.ts
|
|
2752
|
+
var PAGES_EDGE_KEY_NAME = "cookie-key-v1";
|
|
2753
|
+
var PAGES_HOST_HEADER = "x-foundation-pages-host";
|
|
2754
|
+
var PAGES_VIEWER_REQUEST_SOURCE = `import cf from 'cloudfront';
|
|
2755
|
+
import crypto from 'crypto';
|
|
2756
|
+
|
|
2757
|
+
var kvs = cf.kvs('__KVS_ID__');
|
|
2758
|
+
var COOKIE = 'fpa';
|
|
2759
|
+
var KEY_NAME = '${PAGES_EDGE_KEY_NAME}';
|
|
2760
|
+
var HOST_HEADER = '${PAGES_HOST_HEADER}';
|
|
2761
|
+
var PAGE_RE = /^\\/p\\/([a-z0-9]{24})(\\/.*)?$/;
|
|
2762
|
+
var VERSION_RE = /^\\/v([1-9][0-9]{0,5})(\\/.*)?$/;
|
|
2763
|
+
var EXPORT_RE = /^\\/exports\\/v([1-9][0-9]{0,5})\\/([^\\/]+)$/;
|
|
2764
|
+
var UNSAFE_RE = /(^|\\/)\\.\\.?(\\/|$)|\\/\\/|%2e|%2f|%5c|\\\\/i;
|
|
2765
|
+
var USER_RE = /^[UW][A-Z0-9]{2,31}$/;
|
|
2766
|
+
var EXP_RE = /^[0-9]{1,12}$/;
|
|
2767
|
+
|
|
2768
|
+
function reply(status, description, body) {
|
|
2769
|
+
return {
|
|
2770
|
+
statusCode: status,
|
|
2771
|
+
statusDescription: description,
|
|
2772
|
+
headers: {
|
|
2773
|
+
'content-type': { value: 'text/plain; charset=utf-8' },
|
|
2774
|
+
'cache-control': { value: 'no-store' }
|
|
2775
|
+
},
|
|
2776
|
+
body: { encoding: 'text', data: body }
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
function redirect(location) {
|
|
2781
|
+
return {
|
|
2782
|
+
statusCode: 302,
|
|
2783
|
+
statusDescription: 'Found',
|
|
2784
|
+
headers: { location: { value: location }, 'cache-control': { value: 'no-store' } }
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
function equal(a, b) {
|
|
2789
|
+
if (a.length !== b.length) return false;
|
|
2790
|
+
var diff = 0;
|
|
2791
|
+
for (var i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
2792
|
+
return diff === 0;
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
function validCookie(value, pageId, key, now) {
|
|
2796
|
+
if (typeof value !== 'string' || value.length > 256) return false;
|
|
2797
|
+
var parts = value.split('.');
|
|
2798
|
+
if (parts.length !== 5 || parts[0] !== 'v1' || parts[1] !== pageId) return false;
|
|
2799
|
+
if (!USER_RE.test(parts[2]) || !EXP_RE.test(parts[3]) || Number(parts[3]) <= now) return false;
|
|
2800
|
+
var mac = crypto.createHmac('sha256', key).update(parts.slice(0, 4).join('.')).digest('hex');
|
|
2801
|
+
return equal(mac, parts[4]);
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
function cookieValues(request) {
|
|
2805
|
+
var cookie = request.cookies ? request.cookies[COOKIE] : undefined;
|
|
2806
|
+
if (!cookie) return [];
|
|
2807
|
+
var values = [cookie.value];
|
|
2808
|
+
if (cookie.multiValue) {
|
|
2809
|
+
for (var i = 0; i < cookie.multiValue.length; i++) values.push(cookie.multiValue[i].value);
|
|
2810
|
+
}
|
|
2811
|
+
return values;
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
function objectPath(pageId, rest) {
|
|
2815
|
+
var prefix = '/pages/' + pageId;
|
|
2816
|
+
if (rest === '/') return prefix + '/latest.html';
|
|
2817
|
+
var version = VERSION_RE.exec(rest);
|
|
2818
|
+
if (version) {
|
|
2819
|
+
var sub = version[2];
|
|
2820
|
+
if (sub.charAt(sub.length - 1) === '/') {
|
|
2821
|
+
sub = sub + 'index.html';
|
|
2822
|
+
} else if (sub.slice(sub.lastIndexOf('/') + 1).indexOf('.') === -1) {
|
|
2823
|
+
sub = '/index.html';
|
|
2824
|
+
}
|
|
2825
|
+
return prefix + '/v' + version[1] + '/build' + sub;
|
|
2826
|
+
}
|
|
2827
|
+
var download = EXPORT_RE.exec(rest);
|
|
2828
|
+
if (download) return prefix + '/v' + download[1] + '/exports/' + download[2];
|
|
2829
|
+
return null;
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
async function handler(event) {
|
|
2833
|
+
var request = event.request;
|
|
2834
|
+
var uri = request.uri;
|
|
2835
|
+
if (uri.indexOf('/_auth/') === 0) {
|
|
2836
|
+
request.headers[HOST_HEADER] = { value: request.headers.host ? request.headers.host.value : '' };
|
|
2837
|
+
return request;
|
|
2838
|
+
}
|
|
2839
|
+
var page = PAGE_RE.exec(uri);
|
|
2840
|
+
if (!page || UNSAFE_RE.test(uri)) return reply(404, 'Not Found', 'Not found');
|
|
2841
|
+
var pageId = page[1];
|
|
2842
|
+
var rest = page[2] || '';
|
|
2843
|
+
if (rest === '') return redirect(uri + '/');
|
|
2844
|
+
var version = VERSION_RE.exec(rest);
|
|
2845
|
+
if (version && version[2] === undefined) return redirect(uri + '/');
|
|
2846
|
+
var target = objectPath(pageId, rest);
|
|
2847
|
+
if (target === null) return reply(404, 'Not Found', 'Not found');
|
|
2848
|
+
|
|
2849
|
+
var key;
|
|
2850
|
+
try {
|
|
2851
|
+
key = await kvs.get(KEY_NAME);
|
|
2852
|
+
} catch (err) {
|
|
2853
|
+
return reply(503, 'Service Unavailable', 'Pages sign-in is starting up. Try again in a minute.');
|
|
2854
|
+
}
|
|
2855
|
+
var now = Math.floor(Date.now() / 1000);
|
|
2856
|
+
var values = cookieValues(request);
|
|
2857
|
+
for (var i = 0; i < values.length; i++) {
|
|
2858
|
+
if (validCookie(values[i], pageId, key, now)) {
|
|
2859
|
+
request.uri = target;
|
|
2860
|
+
return request;
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
return redirect('/_auth/start?page=' + pageId + '&next=' + encodeURIComponent(uri));
|
|
2864
|
+
}
|
|
2865
|
+
`;
|
|
2866
|
+
function pagesViewerRequestCode(kvsId) {
|
|
2867
|
+
return PAGES_VIEWER_REQUEST_SOURCE.replace("__KVS_ID__", kvsId);
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
// src/stacks/pages-stack.ts
|
|
2871
|
+
function pagesDomainPlan(instance, delegated) {
|
|
2872
|
+
const { domain, hostedZoneId } = instance.pages;
|
|
2873
|
+
if (domain === undefined)
|
|
2874
|
+
return { ownZone: false };
|
|
2875
|
+
const ownZone = hostedZoneId === undefined;
|
|
2876
|
+
return ownZone && !delegated ? { pending: domain, ownZone } : { attached: domain, ownZone };
|
|
2877
|
+
}
|
|
2878
|
+
var PAGES_CONTENT_SECURITY_POLICY = [
|
|
2879
|
+
"default-src 'self'",
|
|
2880
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:",
|
|
2881
|
+
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
|
2882
|
+
"font-src 'self' data: https://fonts.gstatic.com",
|
|
2883
|
+
"img-src 'self' data: blob:",
|
|
2884
|
+
"media-src 'self' data: blob:",
|
|
2885
|
+
"connect-src 'self' data: blob:",
|
|
2886
|
+
"worker-src 'self' blob:",
|
|
2887
|
+
"frame-src 'self'",
|
|
2888
|
+
"object-src 'none'",
|
|
2889
|
+
"base-uri 'self'",
|
|
2890
|
+
"form-action 'self'",
|
|
2891
|
+
"frame-ancestors 'self'"
|
|
2892
|
+
].join("; ");
|
|
2893
|
+
|
|
2894
|
+
class FoundationPages extends cdk7.Stack {
|
|
2895
|
+
bucket;
|
|
2896
|
+
table;
|
|
2897
|
+
apiFunction;
|
|
2898
|
+
distribution;
|
|
2899
|
+
constructor(scope, id, props) {
|
|
2900
|
+
super(scope, id, props);
|
|
2901
|
+
const { instance } = props;
|
|
2902
|
+
if (!instance.integrations.pages)
|
|
2903
|
+
throw new Error(`${id}: FoundationPages needs integrations.pages: true`);
|
|
2904
|
+
const names = instanceNames(instance);
|
|
2905
|
+
const display = instance.displayName;
|
|
2906
|
+
const plan = pagesDomainPlan(instance, props.domainDelegated === true);
|
|
2907
|
+
const domain = plan.attached;
|
|
2908
|
+
this.bucket = new s32.Bucket(this, "PagesBucket", {
|
|
2909
|
+
blockPublicAccess: s32.BlockPublicAccess.BLOCK_ALL,
|
|
2910
|
+
encryption: s32.BucketEncryption.S3_MANAGED,
|
|
2911
|
+
enforceSSL: true,
|
|
2912
|
+
versioned: true,
|
|
2913
|
+
objectOwnership: s32.ObjectOwnership.BUCKET_OWNER_ENFORCED,
|
|
2914
|
+
removalPolicy: cdk7.RemovalPolicy.RETAIN,
|
|
2915
|
+
lifecycleRules: [
|
|
2916
|
+
{ id: "StagedUploads", prefix: "uploads/", expiration: cdk7.Duration.days(1) },
|
|
2917
|
+
{ id: "NoncurrentObjects", noncurrentVersionExpiration: cdk7.Duration.days(30) },
|
|
2918
|
+
{ id: "IncompleteMultipart", abortIncompleteMultipartUploadAfter: cdk7.Duration.days(1) }
|
|
2919
|
+
]
|
|
2920
|
+
});
|
|
2921
|
+
this.table = new dynamodb3.Table(this, "PagesTable", {
|
|
2922
|
+
partitionKey: { name: "pk", type: dynamodb3.AttributeType.STRING },
|
|
2923
|
+
sortKey: { name: "sk", type: dynamodb3.AttributeType.STRING },
|
|
2924
|
+
billingMode: dynamodb3.BillingMode.PAY_PER_REQUEST,
|
|
2925
|
+
encryption: dynamodb3.TableEncryption.CUSTOMER_MANAGED,
|
|
2926
|
+
encryptionKey: props.dataKey,
|
|
2927
|
+
pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: true },
|
|
2928
|
+
timeToLiveAttribute: "ttl",
|
|
2929
|
+
removalPolicy: cdk7.RemovalPolicy.RETAIN
|
|
2930
|
+
});
|
|
2931
|
+
this.table.addGlobalSecondaryIndex({
|
|
2932
|
+
indexName: "ChannelIndex",
|
|
2933
|
+
partitionKey: { name: "gsi1pk", type: dynamodb3.AttributeType.STRING },
|
|
2934
|
+
sortKey: { name: "gsi1sk", type: dynamodb3.AttributeType.STRING },
|
|
2935
|
+
projectionType: dynamodb3.ProjectionType.ALL
|
|
2936
|
+
});
|
|
2937
|
+
const cookieKey = new secretsmanager4.Secret(this, "CookieKey", {
|
|
2938
|
+
secretName: names.secretPagesCookieKey,
|
|
2939
|
+
description: `${display} hosted-pages cookie signing key ({ key }); read by the sign-in Lambda and copied to the edge key store`,
|
|
2940
|
+
removalPolicy: cdk7.RemovalPolicy.DESTROY,
|
|
2941
|
+
generateSecretString: {
|
|
2942
|
+
secretStringTemplate: "{}",
|
|
2943
|
+
generateStringKey: "key",
|
|
2944
|
+
passwordLength: 64,
|
|
2945
|
+
excludePunctuation: true
|
|
2946
|
+
}
|
|
2947
|
+
});
|
|
2948
|
+
const edgeKeys = new cloudfront.KeyValueStore(this, "EdgeKeys", {
|
|
2949
|
+
comment: `${display} pages cookie key for the viewer function`
|
|
2950
|
+
});
|
|
2951
|
+
const authRole = new iam8.Role(this, "PagesAuthRole", {
|
|
2952
|
+
assumedBy: new iam8.ServicePrincipal("lambda.amazonaws.com"),
|
|
2953
|
+
description: `${display} pages sign-in Lambda (Slack OIDC + membership check)`,
|
|
2954
|
+
managedPolicies: [
|
|
2955
|
+
iam8.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
|
|
2956
|
+
]
|
|
2957
|
+
});
|
|
2958
|
+
authRole.addToPolicy(new iam8.PolicyStatement({
|
|
2959
|
+
sid: "PagesAuthSecrets",
|
|
2960
|
+
actions: ["secretsmanager:GetSecretValue"],
|
|
2961
|
+
resources: [secretArnPattern3(props.secrets.slackApp), cookieKey.secretArn]
|
|
2962
|
+
}));
|
|
2963
|
+
authRole.addToPolicy(new iam8.PolicyStatement({
|
|
2964
|
+
sid: "PagesAuthPageLookup",
|
|
2965
|
+
actions: ["dynamodb:GetItem"],
|
|
2966
|
+
resources: [this.table.tableArn]
|
|
2967
|
+
}));
|
|
2968
|
+
authRole.addToPolicy(new iam8.PolicyStatement({
|
|
2969
|
+
sid: "PagesTableKey",
|
|
2970
|
+
actions: ["kms:Decrypt", "kms:DescribeKey"],
|
|
2971
|
+
resources: [props.dataKey.keyArn]
|
|
2972
|
+
}));
|
|
2973
|
+
const authFunction = new lambda3.Function(this, "PagesAuthFunction", {
|
|
2974
|
+
runtime: lambda3.Runtime.NODEJS_22_X,
|
|
2975
|
+
handler: "index.handler",
|
|
2976
|
+
code: lambdaCode(this, "pages-auth"),
|
|
2977
|
+
role: authRole,
|
|
2978
|
+
timeout: cdk7.Duration.seconds(15),
|
|
2979
|
+
memorySize: 256,
|
|
2980
|
+
environment: {
|
|
2981
|
+
FOUNDATION_SLACK_APP_SECRET_ID: props.secrets.slackApp.secretName,
|
|
2982
|
+
FOUNDATION_PAGES_COOKIE_SECRET_ID: cookieKey.secretName,
|
|
2983
|
+
FOUNDATION_SLACK_TEAM_ID: instance.slack.teamId,
|
|
2984
|
+
FOUNDATION_PAGES_TABLE_NAME: this.table.tableName,
|
|
2985
|
+
FOUNDATION_DISPLAY_NAME: display,
|
|
2986
|
+
...domain === undefined ? {} : { FOUNDATION_PAGES_DOMAIN: domain }
|
|
2987
|
+
}
|
|
2988
|
+
});
|
|
2989
|
+
const authUrl = authFunction.addFunctionUrl({ authType: lambda3.FunctionUrlAuthType.AWS_IAM });
|
|
2990
|
+
const seedRole = new iam8.Role(this, "EdgeKeySeedRole", {
|
|
2991
|
+
assumedBy: new iam8.ServicePrincipal("lambda.amazonaws.com"),
|
|
2992
|
+
description: `${display} copies the pages cookie key into the edge key store`,
|
|
2993
|
+
managedPolicies: [
|
|
2994
|
+
iam8.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
|
|
2995
|
+
]
|
|
2996
|
+
});
|
|
2997
|
+
seedRole.addToPolicy(new iam8.PolicyStatement({
|
|
2998
|
+
sid: "ReadCookieKey",
|
|
2999
|
+
actions: ["secretsmanager:GetSecretValue"],
|
|
3000
|
+
resources: [cookieKey.secretArn]
|
|
3001
|
+
}));
|
|
3002
|
+
seedRole.addToPolicy(new iam8.PolicyStatement({
|
|
3003
|
+
sid: "WriteEdgeKey",
|
|
3004
|
+
actions: [
|
|
3005
|
+
"cloudfront-keyvaluestore:DescribeKeyValueStore",
|
|
3006
|
+
"cloudfront-keyvaluestore:PutKey"
|
|
3007
|
+
],
|
|
3008
|
+
resources: [edgeKeys.keyValueStoreArn]
|
|
3009
|
+
}));
|
|
3010
|
+
const seedFunction = new lambda3.Function(this, "EdgeKeySeedFunction", {
|
|
3011
|
+
runtime: lambda3.Runtime.NODEJS_22_X,
|
|
3012
|
+
handler: "index.edgeKeyHandler",
|
|
3013
|
+
code: lambdaCode(this, "pages-auth"),
|
|
3014
|
+
role: seedRole,
|
|
3015
|
+
timeout: cdk7.Duration.seconds(60),
|
|
3016
|
+
memorySize: 256
|
|
3017
|
+
});
|
|
3018
|
+
new cdk7.CustomResource(this, "EdgeKeySeed", {
|
|
3019
|
+
serviceToken: seedFunction.functionArn,
|
|
3020
|
+
resourceType: "Custom::FoundationPagesEdgeKey",
|
|
3021
|
+
properties: {
|
|
3022
|
+
SecretId: cookieKey.secretArn,
|
|
3023
|
+
KeyValueStoreArn: edgeKeys.keyValueStoreArn,
|
|
3024
|
+
Generation: "1"
|
|
3025
|
+
}
|
|
3026
|
+
});
|
|
3027
|
+
const viewerRequest = new cloudfront.Function(this, "ViewerRequest", {
|
|
3028
|
+
runtime: cloudfront.FunctionRuntime.JS_2_0,
|
|
3029
|
+
keyValueStore: edgeKeys,
|
|
3030
|
+
comment: `${display} pages: cookie check and path mapping`,
|
|
3031
|
+
code: cloudfront.FunctionCode.fromInline(pagesViewerRequestCode(edgeKeys.keyValueStoreId))
|
|
3032
|
+
});
|
|
3033
|
+
const headers = new cloudfront.ResponseHeadersPolicy(this, "PagesHeaders", {
|
|
3034
|
+
comment: `${display} pages security headers`,
|
|
3035
|
+
securityHeadersBehavior: {
|
|
3036
|
+
contentSecurityPolicy: {
|
|
3037
|
+
contentSecurityPolicy: PAGES_CONTENT_SECURITY_POLICY,
|
|
3038
|
+
override: true
|
|
3039
|
+
},
|
|
3040
|
+
strictTransportSecurity: {
|
|
3041
|
+
accessControlMaxAge: cdk7.Duration.days(365),
|
|
3042
|
+
includeSubdomains: false,
|
|
3043
|
+
override: true
|
|
3044
|
+
},
|
|
3045
|
+
contentTypeOptions: { override: true },
|
|
3046
|
+
frameOptions: { frameOption: cloudfront.HeadersFrameOption.SAMEORIGIN, override: true },
|
|
3047
|
+
referrerPolicy: {
|
|
3048
|
+
referrerPolicy: cloudfront.HeadersReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
|
|
3049
|
+
override: true
|
|
3050
|
+
}
|
|
3051
|
+
},
|
|
3052
|
+
customHeadersBehavior: {
|
|
3053
|
+
customHeaders: [{ header: "X-Robots-Tag", value: "noindex, nofollow", override: true }]
|
|
3054
|
+
}
|
|
3055
|
+
});
|
|
3056
|
+
const zoneDomain = instance.pages.domain;
|
|
3057
|
+
const zone = zoneDomain === undefined ? undefined : instance.pages.hostedZoneId !== undefined ? route532.HostedZone.fromHostedZoneId(this, "PagesParentZone", instance.pages.hostedZoneId) : new route532.PublicHostedZone(this, "PagesZone", {
|
|
3058
|
+
zoneName: zoneDomain,
|
|
3059
|
+
comment: `${display} hosted pages (delegated from the company's DNS)`
|
|
3060
|
+
});
|
|
3061
|
+
let certificate;
|
|
3062
|
+
if (domain !== undefined && zone !== undefined) {
|
|
3063
|
+
const certificateRole = new iam8.Role(this, "PagesCertificateRole", {
|
|
3064
|
+
assumedBy: new iam8.ServicePrincipal("lambda.amazonaws.com"),
|
|
3065
|
+
description: `${display} issues the pages domain certificate in us-east-1`,
|
|
3066
|
+
managedPolicies: [
|
|
3067
|
+
iam8.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
|
|
3068
|
+
]
|
|
3069
|
+
});
|
|
3070
|
+
certificateRole.addToPolicy(new iam8.PolicyStatement({
|
|
3071
|
+
sid: "RequestCertificate",
|
|
3072
|
+
actions: ["acm:RequestCertificate"],
|
|
3073
|
+
resources: ["*"]
|
|
3074
|
+
}));
|
|
3075
|
+
certificateRole.addToPolicy(new iam8.PolicyStatement({
|
|
3076
|
+
sid: "ManageCertificate",
|
|
3077
|
+
actions: ["acm:DescribeCertificate", "acm:DeleteCertificate"],
|
|
3078
|
+
resources: [`arn:${this.partition}:acm:us-east-1:${this.account}:certificate/*`]
|
|
3079
|
+
}));
|
|
3080
|
+
certificateRole.addToPolicy(new iam8.PolicyStatement({
|
|
3081
|
+
sid: "ValidationRecord",
|
|
3082
|
+
actions: ["route53:ChangeResourceRecordSets"],
|
|
3083
|
+
resources: [`arn:${this.partition}:route53:::hostedzone/${zone.hostedZoneId}`]
|
|
3084
|
+
}));
|
|
3085
|
+
const certificateFunction = new lambda3.Function(this, "PagesCertificateFunction", {
|
|
3086
|
+
runtime: lambda3.Runtime.NODEJS_22_X,
|
|
3087
|
+
handler: "index.certificateHandler",
|
|
3088
|
+
code: lambdaCode(this, "pages-auth"),
|
|
3089
|
+
role: certificateRole,
|
|
3090
|
+
timeout: cdk7.Duration.minutes(15),
|
|
3091
|
+
memorySize: 256
|
|
3092
|
+
});
|
|
3093
|
+
const issued = new cdk7.CustomResource(this, "PagesCertificate", {
|
|
3094
|
+
serviceToken: certificateFunction.functionArn,
|
|
3095
|
+
resourceType: "Custom::FoundationPagesCertificate",
|
|
3096
|
+
properties: { DomainName: domain, HostedZoneId: zone.hostedZoneId }
|
|
3097
|
+
});
|
|
3098
|
+
certificate = acm2.Certificate.fromCertificateArn(this, "PagesCertificateArn", issued.getAttString("CertificateArn"));
|
|
3099
|
+
}
|
|
3100
|
+
const functionAssociations = [
|
|
3101
|
+
{ function: viewerRequest, eventType: cloudfront.FunctionEventType.VIEWER_REQUEST }
|
|
3102
|
+
];
|
|
3103
|
+
this.distribution = new cloudfront.Distribution(this, "Distribution", {
|
|
3104
|
+
comment: `${display} pages`,
|
|
3105
|
+
...domain === undefined || certificate === undefined ? {} : { domainNames: [domain], certificate },
|
|
3106
|
+
defaultRootObject: "index.html",
|
|
3107
|
+
minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
|
|
3108
|
+
httpVersion: cloudfront.HttpVersion.HTTP2_AND_3,
|
|
3109
|
+
priceClass: cloudfront.PriceClass.PRICE_CLASS_100,
|
|
3110
|
+
defaultBehavior: {
|
|
3111
|
+
origin: origins.S3BucketOrigin.withOriginAccessControl(this.bucket, {
|
|
3112
|
+
originAccessLevels: [cloudfront.AccessLevel.READ, cloudfront.AccessLevel.LIST]
|
|
3113
|
+
}),
|
|
3114
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
3115
|
+
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
|
|
3116
|
+
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
|
|
3117
|
+
responseHeadersPolicy: headers,
|
|
3118
|
+
functionAssociations
|
|
3119
|
+
},
|
|
3120
|
+
additionalBehaviors: {
|
|
3121
|
+
"/_auth/*": {
|
|
3122
|
+
origin: origins.FunctionUrlOrigin.withOriginAccessControl(authUrl),
|
|
3123
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
3124
|
+
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
|
|
3125
|
+
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
|
|
3126
|
+
originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
|
|
3127
|
+
responseHeadersPolicy: headers,
|
|
3128
|
+
functionAssociations
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
});
|
|
3132
|
+
cdk7.Annotations.of(this.distribution).acknowledgeWarning("@aws-cdk/aws-cloudfront-origins:listBucketSecurityRisk", "The viewer function forwards only mapped object keys and 404s `/`; defaultRootObject is set too. LIST only turns a missing key into 404 instead of 403.");
|
|
3133
|
+
if (domain !== undefined && zone !== undefined) {
|
|
3134
|
+
const target = route532.RecordTarget.fromAlias(new route53Targets2.CloudFrontTarget(this.distribution));
|
|
3135
|
+
new route532.ARecord(this, "PagesAliasA", { zone, recordName: `${domain}.`, target });
|
|
3136
|
+
new route532.AaaaRecord(this, "PagesAliasAaaa", { zone, recordName: `${domain}.`, target });
|
|
3137
|
+
}
|
|
3138
|
+
const defaultUrl = `https://${this.distribution.distributionDomainName}`;
|
|
3139
|
+
const baseUrl = domain === undefined ? defaultUrl : `https://${domain}`;
|
|
3140
|
+
const apiRole = new iam8.Role(this, "PagesApiRole", {
|
|
3141
|
+
assumedBy: new iam8.ServicePrincipal("lambda.amazonaws.com"),
|
|
3142
|
+
description: `${display} Pages API proxy (the only writer of the pages bucket and table)`,
|
|
3143
|
+
managedPolicies: [
|
|
3144
|
+
iam8.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
|
|
3145
|
+
]
|
|
3146
|
+
});
|
|
3147
|
+
apiRole.addToPolicy(new iam8.PolicyStatement({
|
|
3148
|
+
sid: "PagesApiSigningSecret",
|
|
3149
|
+
actions: ["secretsmanager:GetSecretValue"],
|
|
3150
|
+
resources: [secretArnPattern3(props.secrets.signing)]
|
|
3151
|
+
}));
|
|
3152
|
+
apiRole.addToPolicy(new iam8.PolicyStatement({
|
|
3153
|
+
sid: "PagesTableData",
|
|
3154
|
+
actions: [
|
|
3155
|
+
"dynamodb:GetItem",
|
|
3156
|
+
"dynamodb:PutItem",
|
|
3157
|
+
"dynamodb:UpdateItem",
|
|
3158
|
+
"dynamodb:Query",
|
|
3159
|
+
"dynamodb:TransactWriteItems",
|
|
3160
|
+
"dynamodb:ConditionCheckItem"
|
|
3161
|
+
],
|
|
3162
|
+
resources: [this.table.tableArn]
|
|
3163
|
+
}));
|
|
3164
|
+
apiRole.addToPolicy(new iam8.PolicyStatement({
|
|
3165
|
+
sid: "PagesTableKey",
|
|
3166
|
+
actions: ["kms:Decrypt", "kms:Encrypt", "kms:GenerateDataKey", "kms:DescribeKey"],
|
|
3167
|
+
resources: [props.dataKey.keyArn]
|
|
3168
|
+
}));
|
|
3169
|
+
apiRole.addToPolicy(new iam8.PolicyStatement({
|
|
3170
|
+
sid: "PagesObjects",
|
|
3171
|
+
actions: ["s3:GetObject", "s3:PutObject"],
|
|
3172
|
+
resources: [this.bucket.arnForObjects("pages/*"), this.bucket.arnForObjects("uploads/*")]
|
|
3173
|
+
}));
|
|
3174
|
+
this.apiFunction = new lambda3.Function(this, "PagesApiFunction", {
|
|
3175
|
+
functionName: names.pagesApiFunctionName,
|
|
3176
|
+
runtime: lambda3.Runtime.NODEJS_22_X,
|
|
3177
|
+
handler: "index.handler",
|
|
3178
|
+
code: lambdaCode(this, "pages-api"),
|
|
3179
|
+
role: apiRole,
|
|
3180
|
+
timeout: cdk7.Duration.seconds(300),
|
|
3181
|
+
memorySize: 1024,
|
|
3182
|
+
environment: {
|
|
3183
|
+
FOUNDATION_PAGES_TABLE_NAME: this.table.tableName,
|
|
3184
|
+
FOUNDATION_PAGES_BUCKET_NAME: this.bucket.bucketName,
|
|
3185
|
+
FOUNDATION_PAGES_BASE_URL: baseUrl,
|
|
3186
|
+
FOUNDATION_SLACK_TEAM_ID: instance.slack.teamId,
|
|
3187
|
+
FOUNDATION_SLACK_SIGNING_SECRET_ID: props.secrets.signing.secretName
|
|
3188
|
+
}
|
|
3189
|
+
});
|
|
3190
|
+
new cdk7.CfnOutput(this, "PagesApiFunctionArn", { value: this.apiFunction.functionArn });
|
|
3191
|
+
new cdk7.CfnOutput(this, "PagesBaseUrl", { value: baseUrl });
|
|
3192
|
+
new cdk7.CfnOutput(this, "PagesDistributionDomainName", {
|
|
3193
|
+
value: this.distribution.distributionDomainName
|
|
3194
|
+
});
|
|
3195
|
+
new cdk7.CfnOutput(this, "PagesAuthRedirectUrl", {
|
|
3196
|
+
description: "Slack OAuth redirect URL for Sign in with Slack on the pages domain",
|
|
3197
|
+
value: `${baseUrl}/_auth/callback`
|
|
3198
|
+
});
|
|
3199
|
+
new cdk7.CfnOutput(this, "PagesDefaultAuthRedirectUrl", {
|
|
3200
|
+
value: `${defaultUrl}/_auth/callback`
|
|
3201
|
+
});
|
|
3202
|
+
new cdk7.CfnOutput(this, "PagesCustomDomainAttached", {
|
|
3203
|
+
value: domain === undefined ? "false" : "true"
|
|
3204
|
+
});
|
|
3205
|
+
if (domain !== undefined)
|
|
3206
|
+
new cdk7.CfnOutput(this, "PagesCustomDomain", { value: domain });
|
|
3207
|
+
if (zone instanceof route532.PublicHostedZone) {
|
|
3208
|
+
new cdk7.CfnOutput(this, "PagesHostedZoneId", { value: zone.hostedZoneId });
|
|
3209
|
+
new cdk7.CfnOutput(this, "PagesNameServers", {
|
|
3210
|
+
description: `NS records to add for ${zoneDomain} at the company's DNS provider`,
|
|
3211
|
+
value: cdk7.Fn.join(",", zone.hostedZoneNameServers ?? [])
|
|
3212
|
+
});
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
function secretArnPattern3(secret) {
|
|
3217
|
+
return secret.secretFullArn ?? `${secret.secretArn}-??????`;
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
// src/stacks/pipeline-stack.ts
|
|
3221
|
+
import * as cdk8 from "aws-cdk-lib";
|
|
2684
3222
|
import * as codebuild from "aws-cdk-lib/aws-codebuild";
|
|
2685
3223
|
import * as codepipeline from "aws-cdk-lib/aws-codepipeline";
|
|
2686
3224
|
import * as actions from "aws-cdk-lib/aws-codepipeline-actions";
|
|
2687
|
-
import * as
|
|
3225
|
+
import * as iam9 from "aws-cdk-lib/aws-iam";
|
|
2688
3226
|
import * as sns3 from "aws-cdk-lib/aws-sns";
|
|
2689
3227
|
import * as subscriptions from "aws-cdk-lib/aws-sns-subscriptions";
|
|
2690
3228
|
var DEFAULT_BUILDSPEC_PATH = "buildspec.yml";
|
|
2691
|
-
var BUILD_TIMEOUT =
|
|
3229
|
+
var BUILD_TIMEOUT = cdk8.Duration.minutes(60);
|
|
2692
3230
|
|
|
2693
|
-
class FoundationPipeline extends
|
|
3231
|
+
class FoundationPipeline extends cdk8.Stack {
|
|
2694
3232
|
pipeline;
|
|
2695
3233
|
buildRole;
|
|
2696
3234
|
constructor(scope, id, props) {
|
|
@@ -2701,11 +3239,11 @@ class FoundationPipeline extends cdk7.Stack {
|
|
|
2701
3239
|
if (deploy.connectionArn === undefined)
|
|
2702
3240
|
throw new Error(`instance ${props.instance.name}: deploy.connectionArn is required for a pipeline`);
|
|
2703
3241
|
const [owner = "", repo = ""] = props.instance.github.repo.split("/");
|
|
2704
|
-
this.buildRole = new
|
|
3242
|
+
this.buildRole = new iam9.Role(this, "BuildRole", {
|
|
2705
3243
|
description: `CodePipeline deployer for ${props.instance.name}`,
|
|
2706
|
-
assumedBy: new
|
|
3244
|
+
assumedBy: new iam9.ServicePrincipal("codebuild.amazonaws.com"),
|
|
2707
3245
|
inlinePolicies: {
|
|
2708
|
-
FoundationDeploy: new
|
|
3246
|
+
FoundationDeploy: new iam9.PolicyDocument({
|
|
2709
3247
|
statements: deployStatements(this, names, { bucket: props.bucket, dataKey: props.dataKey }, props.instance)
|
|
2710
3248
|
})
|
|
2711
3249
|
}
|
|
@@ -2799,11 +3337,11 @@ class FoundationPipeline extends cdk7.Stack {
|
|
|
2799
3337
|
this.pipeline.onStateChange("OnFailure", {
|
|
2800
3338
|
description: `${props.instance.name} deploy failed`,
|
|
2801
3339
|
eventPattern: { detail: { state: ["FAILED"] } },
|
|
2802
|
-
target: new
|
|
3340
|
+
target: new cdk8.aws_events_targets.SnsTopic(failureTopic)
|
|
2803
3341
|
});
|
|
2804
|
-
new
|
|
2805
|
-
new
|
|
3342
|
+
new cdk8.CfnOutput(this, "PipelineName", { value: this.pipeline.pipelineName });
|
|
3343
|
+
new cdk8.CfnOutput(this, "BuildRoleArn", { value: this.buildRole.roleArn });
|
|
2806
3344
|
}
|
|
2807
3345
|
}
|
|
2808
3346
|
|
|
2809
|
-
export { FoundationAgent, FoundationApi, deployStatements, FoundationCi, FoundationData, FoundationNetwork, NewsletterStack, DEFAULT_BUILDSPEC_PATH, BUILD_TIMEOUT, FoundationPipeline };
|
|
3347
|
+
export { FoundationAgent, FoundationApi, deployStatements, FoundationCi, FoundationData, FoundationNetwork, NewsletterStack, FoundationPages, DEFAULT_BUILDSPEC_PATH, BUILD_TIMEOUT, FoundationPipeline };
|
package/dist/src/index.js
CHANGED
|
@@ -6,10 +6,11 @@ import {
|
|
|
6
6
|
FoundationCi,
|
|
7
7
|
FoundationData,
|
|
8
8
|
FoundationNetwork,
|
|
9
|
+
FoundationPages,
|
|
9
10
|
FoundationPipeline,
|
|
10
11
|
NewsletterStack,
|
|
11
12
|
deployStatements
|
|
12
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-rq4dc5ns.js";
|
|
13
14
|
import {
|
|
14
15
|
BUN_VERSION,
|
|
15
16
|
LAMBDA_ENTRY_POINTS,
|
|
@@ -42,7 +43,7 @@ import {
|
|
|
42
43
|
skillsKey,
|
|
43
44
|
skipBundle,
|
|
44
45
|
verifyManifest
|
|
45
|
-
} from "../chunk-
|
|
46
|
+
} from "../chunk-9rasrppa.js";
|
|
46
47
|
export {
|
|
47
48
|
BUILD_TIMEOUT,
|
|
48
49
|
BUN_VERSION,
|
|
@@ -52,6 +53,7 @@ export {
|
|
|
52
53
|
FoundationCi,
|
|
53
54
|
FoundationData,
|
|
54
55
|
FoundationNetwork,
|
|
56
|
+
FoundationPages,
|
|
55
57
|
FoundationPipeline,
|
|
56
58
|
LAMBDA_ENTRY_POINTS,
|
|
57
59
|
LIVE_ENDPOINT_NAME,
|
package/package.json
CHANGED
package/src/artifacts.ts
CHANGED
|
@@ -66,6 +66,8 @@ export const LAMBDA_ENTRY_POINTS = {
|
|
|
66
66
|
"crm-proxy": { entry: "packages/capability-crm/src/proxy/handler.ts" },
|
|
67
67
|
"upwork-proxy": { entry: "packages/capability-upwork/src/proxy/handler.ts" },
|
|
68
68
|
"etrade-proxy": { entry: "packages/capability-etrade/src/proxy/handler.ts" },
|
|
69
|
+
"pages-api": { entry: "packages/capability-pages/src/proxy/handler.ts" },
|
|
70
|
+
"pages-auth": { entry: "packages/capability-pages/src/auth/lambda.ts" },
|
|
69
71
|
"browser-proxy": { entry: "packages/gateway/src/browser-proxy.ts", playwright: true },
|
|
70
72
|
"newsletter-public": { entry: "packages/newsletter/src/lambda-public.ts" },
|
|
71
73
|
"newsletter-campaign": { entry: "packages/newsletter/src/lambda-campaign.ts" },
|
package/src/deploy/cli.ts
CHANGED
|
@@ -149,6 +149,9 @@ export async function main(argv: readonly string[] = process.argv.slice(2)): Pro
|
|
|
149
149
|
...(process.env.INTERACTIVE_REQUEST_URL === undefined
|
|
150
150
|
? {}
|
|
151
151
|
: { INTERACTIVE_REQUEST_URL: process.env.INTERACTIVE_REQUEST_URL }),
|
|
152
|
+
...(process.env.PAGES_AUTH_REDIRECT_URL === undefined
|
|
153
|
+
? {}
|
|
154
|
+
: { PAGES_AUTH_REDIRECT_URL: process.env.PAGES_AUTH_REDIRECT_URL }),
|
|
152
155
|
}),
|
|
153
156
|
),
|
|
154
157
|
);
|