canopycms-cdk 0.0.68-int.100 → 0.0.68-int.102
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.
|
@@ -127,6 +127,29 @@ export interface AssetSupportProps {
|
|
|
127
127
|
* @default false
|
|
128
128
|
*/
|
|
129
129
|
readonly autoDeleteObjects?: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Deny every `s3:PutObject` to `assets/*`, `asset-originals/*` and `asset-meta/*` that carries
|
|
132
|
+
* no `If-None-Match`, so no principal can replace an object under those content-addressed
|
|
133
|
+
* prefixes. Standalone mode only: with a BYO `bucket` the prop is refused, and the same statement
|
|
134
|
+
* belongs in that bucket's own policy (see docs/deploying-to-aws.md).
|
|
135
|
+
*
|
|
136
|
+
* Opt-in because the Deny binds every writer: the CMS Lambda, which runs the `canopycms` your app
|
|
137
|
+
* installs, and with `lazyPublicTransforms` the transform Lambda, which bundles the `canopycms`
|
|
138
|
+
* this package was built with and calls S3 through the Lambda runtime's AWS SDK. Upgrade and
|
|
139
|
+
* deploy both to releases whose stores write create-only, then enable this. It also denies every
|
|
140
|
+
* unconditional write, such as a plain `aws s3 cp`, and every multipart upload (`UploadPart`
|
|
141
|
+
* carries no `If-None-Match`). Replication is authorized as `s3:ReplicateObject`, so it is not
|
|
142
|
+
* denied; a replica bucket takes the same statement.
|
|
143
|
+
*
|
|
144
|
+
* Create-only holds only as far as `s3:DeleteObject` does: a delete then a create replaces an
|
|
145
|
+
* object, so keep Delete on `assets/*` and `asset-originals/*` to a break-glass role. That is
|
|
146
|
+
* also how a bad derivative is replaced: delete it, then rerun `materialize-assets`. On a
|
|
147
|
+
* versioned bucket the delete writes a delete marker and the bad version stays restorable; a
|
|
148
|
+
* replica keeps the bad version until the rematerialized object replicates.
|
|
149
|
+
*
|
|
150
|
+
* @default false
|
|
151
|
+
*/
|
|
152
|
+
readonly enforceCreateOnlyWrites?: boolean;
|
|
130
153
|
/**
|
|
131
154
|
* A replica of the bucket's `assets/` prefix. Both public behaviors fail over
|
|
132
155
|
* to it on 500, 502, 503 or 504 from the primary, never on a miss: the
|
|
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { Construct } from 'constructs';
|
|
5
|
-
import { Duration, RemovalPolicy, Stack, Token, aws_cloudfront as cloudfront, aws_cloudfront_origins as origins, aws_lambda as lambda, aws_logs as logs, aws_s3 as s3, } from 'aws-cdk-lib';
|
|
5
|
+
import { Duration, RemovalPolicy, Stack, Token, aws_cloudfront as cloudfront, aws_cloudfront_origins as origins, aws_iam as iam, aws_lambda as lambda, aws_logs as logs, aws_s3 as s3, } from 'aws-cdk-lib';
|
|
6
6
|
import { attachLambdaExecutionPolicies } from './lambda-execution-role.js';
|
|
7
7
|
// This package (`canopycms-cdk`) is `"type": "module"`, so its compiled
|
|
8
8
|
// output is real ESM - `__dirname` is not a global there. Derive it from
|
|
@@ -42,6 +42,8 @@ const PREFIXES = {
|
|
|
42
42
|
public: 'assets',
|
|
43
43
|
transform: 'assets/t',
|
|
44
44
|
};
|
|
45
|
+
/** The content-addressed prefixes, whose objects no correct writer ever replaces. */
|
|
46
|
+
const CREATE_ONLY_PREFIXES = [PREFIXES.public, PREFIXES.originals, PREFIXES.meta];
|
|
45
47
|
/**
|
|
46
48
|
* The tag on every object the transform Lambda writes, copied as literals for
|
|
47
49
|
* the same reason as `PREFIXES`. Source of truth is `LAZY_TRANSFORM_TAG` in
|
|
@@ -519,6 +521,13 @@ export class AssetSupport extends Construct {
|
|
|
519
521
|
'`lazyPublicTransforms` and run `canopycms materialize-assets` in your release ' +
|
|
520
522
|
'pipeline instead.');
|
|
521
523
|
}
|
|
524
|
+
if (props.bucket && props.enforceCreateOnlyWrites) {
|
|
525
|
+
throw new Error('AssetSupport: `enforceCreateOnlyWrites` writes its Deny into the policy of a bucket this ' +
|
|
526
|
+
'construct creates, and cannot write one on a BYO `bucket`. Drop the prop and add the ' +
|
|
527
|
+
"statement to your bucket's own policy: Deny s3:PutObject to any principal on " +
|
|
528
|
+
`${CREATE_ONLY_PREFIXES.map((prefix) => `${prefix}/*`).join(', ')} under the condition ` +
|
|
529
|
+
'{ Null: { "s3:if-none-match": "true" } }.');
|
|
530
|
+
}
|
|
522
531
|
// Fail closed: a bundle without the marker has no linux/arm64 sharp and
|
|
523
532
|
// throws at cold start on the first image request, far from the cause.
|
|
524
533
|
// `pnpm test` leaves exactly such a fixture on disk.
|
|
@@ -610,6 +619,16 @@ export class AssetSupport extends Construct {
|
|
|
610
619
|
: undefined,
|
|
611
620
|
});
|
|
612
621
|
}
|
|
622
|
+
if (props.enforceCreateOnlyWrites) {
|
|
623
|
+
this.bucket.addToResourcePolicy(new iam.PolicyStatement({
|
|
624
|
+
sid: 'DenyAssetOverwrites',
|
|
625
|
+
effect: iam.Effect.DENY,
|
|
626
|
+
principals: [new iam.StarPrincipal()],
|
|
627
|
+
actions: ['s3:PutObject'],
|
|
628
|
+
resources: CREATE_ONLY_PREFIXES.map((prefix) => this.bucket.arnForObjects(`${prefix}/*`)),
|
|
629
|
+
conditions: { Null: { 's3:if-none-match': 'true' } },
|
|
630
|
+
}));
|
|
631
|
+
}
|
|
613
632
|
if (lazy) {
|
|
614
633
|
const transform = this.buildTransformLambda(props);
|
|
615
634
|
this.transformFunction = transform.fn;
|
|
@@ -76,8 +76,8 @@ export interface CanopyCmsAttachOptions {
|
|
|
76
76
|
*
|
|
77
77
|
* It must set `x-forwarded-host` from the viewer's Host, or sign-in
|
|
78
78
|
* redirects point at the Function URL and 403. It must not gate on HTTP
|
|
79
|
-
* Basic auth
|
|
80
|
-
*
|
|
79
|
+
* Basic auth; the CMS authenticates these routes itself. A Basic-gated site sets
|
|
80
|
+
* the CMS config's `unauthenticatedStatus: 419` (see docs/deploying-to-aws.md).
|
|
81
81
|
*
|
|
82
82
|
* @default - a function that sets `x-forwarded-host` and nothing else
|
|
83
83
|
*/
|