@fjall/components-infrastructure 2.14.0 → 2.16.0
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/dist/lib/config/aws/scpPreset.js +9 -0
- package/dist/lib/patterns/aws/storage.d.ts +4 -2
- package/dist/lib/patterns/aws/storage.js +1 -0
- package/dist/lib/resources/aws/compute/ecsImages.js +2 -2
- package/dist/lib/resources/aws/networking/hostedZone.js +12 -1
- package/dist/lib/resources/aws/storage/s3.d.ts +25 -0
- package/dist/lib/resources/aws/storage/s3.js +22 -1
- package/package.json +4 -4
|
@@ -32,6 +32,12 @@ function buildFoundationGuardrails(allowedRegions) {
|
|
|
32
32
|
// Deny + NotAction denies everything except the policy-management
|
|
33
33
|
// scopes a root-task unlock session needs; the AWS root-task policies
|
|
34
34
|
// constrain those sessions further.
|
|
35
|
+
// iam:CreateLoginProfile/GetLoginProfile (root-credential recovery on
|
|
36
|
+
// assume-root sessions) are deliberately NOT carved out — recovery on
|
|
37
|
+
// this tier is a break-glass SCP detach, not a standing carve-out. Do
|
|
38
|
+
// not add them without revisiting the decision:
|
|
39
|
+
// aiDocs/troubleshooting/centralised-root-access-break-glass-runbook.md
|
|
40
|
+
// § Scenario 3.
|
|
35
41
|
NotAction: [
|
|
36
42
|
"s3:GetBucketPolicy",
|
|
37
43
|
"s3:PutBucketPolicy",
|
|
@@ -214,6 +220,9 @@ function buildEncryptionAndAccess() {
|
|
|
214
220
|
{
|
|
215
221
|
Sid: "DenyIamUserCreation",
|
|
216
222
|
Effect: "Deny",
|
|
223
|
+
// Also blocks iam:CreateLoginProfile for assume-root recovery
|
|
224
|
+
// sessions (they match none of EXEMPT_ROLE_PATTERNS) — deliberate;
|
|
225
|
+
// see the DenyRootUserActions note + break-glass runbook § Scenario 3.
|
|
217
226
|
Action: [
|
|
218
227
|
"iam:CreateUser",
|
|
219
228
|
"iam:CreateAccessKey",
|
|
@@ -2,12 +2,12 @@ import { Construct } from "constructs";
|
|
|
2
2
|
import { type IBucket, type EventType, type IBucketNotificationDestination, type NotificationKeyFilter } from "aws-cdk-lib/aws-s3";
|
|
3
3
|
import { type IGrantable, type Grant } from "aws-cdk-lib/aws-iam";
|
|
4
4
|
import type App from "../../app.js";
|
|
5
|
-
import { BucketDeployment, type WebsiteHostingConfig } from "../../resources/aws/storage/index.js";
|
|
5
|
+
import { BucketDeployment, type ResourcePolicyStatement, type WebsiteHostingConfig } from "../../resources/aws/storage/index.js";
|
|
6
6
|
import { type BackupTier } from "../../utils/backupTierMapping.js";
|
|
7
7
|
import { type IStorage } from "./interfaces/storage.js";
|
|
8
8
|
import { type IStorageConnector } from "./interfaces/connector.js";
|
|
9
9
|
export { type IStorage, isStorage } from "./interfaces/storage.js";
|
|
10
|
-
export { type WebsiteHostingConfig } from "../../resources/aws/storage/index.js";
|
|
10
|
+
export { type ResourcePolicyStatement, type WebsiteHostingConfig } from "../../resources/aws/storage/index.js";
|
|
11
11
|
export interface CorsRule {
|
|
12
12
|
readonly allowedOrigins: string[];
|
|
13
13
|
readonly allowedMethods: string[];
|
|
@@ -40,6 +40,8 @@ export interface S3Props {
|
|
|
40
40
|
readonly deployment?: S3DeploymentConfig;
|
|
41
41
|
/** When true, sets RemovalPolicy.RETAIN (overriding the env-aware default). Used for imported buckets. */
|
|
42
42
|
readonly retain?: boolean;
|
|
43
|
+
/** Declarative bucket-policy statements appended to the bucket's resource policy. */
|
|
44
|
+
readonly resourcePolicyStatements?: ResourcePolicyStatement[];
|
|
43
45
|
}
|
|
44
46
|
export interface StorageBuildProps extends S3Props {
|
|
45
47
|
readonly stackPlacement?: "storage" | "cdn" | "compute";
|
|
@@ -66,6 +66,7 @@ export class Storage extends Construct {
|
|
|
66
66
|
backupVaultTier: props.backupVaultTier,
|
|
67
67
|
publicReadAccess: props.publicReadAccess,
|
|
68
68
|
websiteHosting: props.websiteHosting,
|
|
69
|
+
resourcePolicyStatements: props.resourcePolicyStatements,
|
|
69
70
|
...(props.cors && { cors: toCorsRules(props.cors) }),
|
|
70
71
|
...(props.retain && { removalPolicy: RemovalPolicy.RETAIN })
|
|
71
72
|
});
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { CfnParameter, Stack } from "aws-cdk-lib";
|
|
2
2
|
import { ContainerImage } from "aws-cdk-lib/aws-ecs";
|
|
3
3
|
import { Repository } from "aws-cdk-lib/aws-ecr";
|
|
4
|
+
import { imageTagParameterName } from "@fjall/util";
|
|
4
5
|
import { DEFAULT_ECS_FALLBACK_IMAGE } from "./ecsConstants.js";
|
|
5
|
-
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
6
6
|
function buildImageTagDescription(serviceName) {
|
|
7
7
|
return `Image tag for ECS service ${serviceName}. Set by fjall deploy to the content-hash tag.`;
|
|
8
8
|
}
|
|
9
9
|
function getOrCreateImageTagParameter(ctx, serviceName) {
|
|
10
10
|
const stack = Stack.of(ctx.scope);
|
|
11
|
-
const paramLogicalId =
|
|
11
|
+
const paramLogicalId = imageTagParameterName(serviceName);
|
|
12
12
|
const description = buildImageTagDescription(serviceName);
|
|
13
13
|
const existing = stack.node.tryFindChild(paramLogicalId);
|
|
14
14
|
if (existing instanceof CfnParameter) {
|
|
@@ -5,6 +5,7 @@ import { getDomainExportNames } from "@fjall/util";
|
|
|
5
5
|
import { toPascalCase, getSafeZoneName } from "../../../utils/capitaliseString.js";
|
|
6
6
|
import { DelegationRole } from "../iam/delegationRole.js";
|
|
7
7
|
import { applyCostAllocationTags } from "../../../utils/costAllocationTags.js";
|
|
8
|
+
import { resolveOrgId } from "../../../utils/cdkContext.js";
|
|
8
9
|
export class HostedZoneFactory {
|
|
9
10
|
static import(stack, hostedZoneId, zoneName, opts) {
|
|
10
11
|
const safeZone = toPascalCase(getSafeZoneName(zoneName));
|
|
@@ -49,7 +50,17 @@ export class HostedZone extends Construct {
|
|
|
49
50
|
value: Fn.join(",", created.hostedZoneNameServers ?? []),
|
|
50
51
|
exportName: exportNames.nameservers
|
|
51
52
|
});
|
|
52
|
-
|
|
53
|
+
// Org-gate: a single account has no OrganisationId export, so an org-trusting
|
|
54
|
+
// delegation role would leave an unresolved Fn::ImportValue and roll the stack
|
|
55
|
+
// back. Default follows org presence; explicit `true` opts in (and fails fast).
|
|
56
|
+
const inOrganisation = resolveOrgId(this.node) !== undefined;
|
|
57
|
+
if (props.createDelegationRole === true && !inOrganisation) {
|
|
58
|
+
throw new Error(`HostedZone "${props.zoneName}": createDelegationRole was requested but ` +
|
|
59
|
+
`this account is not part of an AWS Organization (no "orgId" context). ` +
|
|
60
|
+
`Cross-account DNS delegation requires an organisation — omit ` +
|
|
61
|
+
`createDelegationRole for single-account setups, or connect an organisation.`);
|
|
62
|
+
}
|
|
63
|
+
if (props.createDelegationRole ?? inOrganisation) {
|
|
53
64
|
this.delegationRole = new DelegationRole(this, `${safeZone}DelegationRole`, {
|
|
54
65
|
zoneName: props.zoneName,
|
|
55
66
|
hostedZone: created,
|
|
@@ -6,6 +6,25 @@ export interface WebsiteHostingConfig {
|
|
|
6
6
|
readonly indexDocument: string;
|
|
7
7
|
readonly errorDocument?: string;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* A single bucket-policy statement expressed declaratively. {@link S3Bucket}
|
|
11
|
+
* turns each entry into an `addToResourcePolicy` call at synth.
|
|
12
|
+
*
|
|
13
|
+
* Principals are either `"*"` (anonymous / public) or an IAM ARN (account root,
|
|
14
|
+
* role, or user). Service and Federated principals are not yet modelled — a
|
|
15
|
+
* remediation that meets one must fall back to report-only rather than dropping
|
|
16
|
+
* it silently. Mirrors the codemod's `S3ResourcePlanSchema.resourcePolicyStatements`
|
|
17
|
+
* in `@fjall/generator` — the two shapes must move together.
|
|
18
|
+
*/
|
|
19
|
+
export interface ResourcePolicyStatement {
|
|
20
|
+
readonly sid?: string;
|
|
21
|
+
readonly effect: "Allow" | "Deny";
|
|
22
|
+
readonly principals: string[];
|
|
23
|
+
readonly actions: string[];
|
|
24
|
+
/** Defaults to the bucket itself and its objects when omitted. */
|
|
25
|
+
readonly resources?: string[];
|
|
26
|
+
readonly conditions?: Record<string, Record<string, string | string[]>>;
|
|
27
|
+
}
|
|
9
28
|
/**
|
|
10
29
|
* Props for {@link S3Bucket}.
|
|
11
30
|
*
|
|
@@ -17,6 +36,12 @@ export interface S3BucketProps extends BucketProps {
|
|
|
17
36
|
backupVaultTier?: BackupTier;
|
|
18
37
|
publicReadAccess?: boolean;
|
|
19
38
|
websiteHosting?: WebsiteHostingConfig;
|
|
39
|
+
/**
|
|
40
|
+
* Declarative bucket-policy statements, each appended via
|
|
41
|
+
* `addToResourcePolicy`. The TLS-only `enforceSSL` deny is always applied
|
|
42
|
+
* separately and must NOT be listed here.
|
|
43
|
+
*/
|
|
44
|
+
resourcePolicyStatements?: ResourcePolicyStatement[];
|
|
20
45
|
}
|
|
21
46
|
export declare class S3Bucket extends Bucket {
|
|
22
47
|
readonly backupVaultTier?: BackupTier;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SDK_PRE_EMPTY_TAG_KEY } from "@fjall/util/aws";
|
|
2
2
|
import { Annotations, CfnOutput, Duration, RemovalPolicy, Tags } from "aws-cdk-lib";
|
|
3
3
|
import { BlockPublicAccess, Bucket } from "aws-cdk-lib/aws-s3";
|
|
4
|
+
import { ArnPrincipal, Effect, PolicyStatement, StarPrincipal } from "aws-cdk-lib/aws-iam";
|
|
4
5
|
import { RegionInfo } from "aws-cdk-lib/region-info";
|
|
5
6
|
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
6
7
|
import { envAwareRemovalPolicyDefault, toRemovalPolicy } from "../../../utils/removalPolicy.js";
|
|
@@ -8,10 +9,15 @@ export { SDK_PRE_EMPTY_TAG_KEY } from "@fjall/util/aws";
|
|
|
8
9
|
function shouldAutoVersion(tier) {
|
|
9
10
|
return tier === "resilient" || tier === "enterprise";
|
|
10
11
|
}
|
|
12
|
+
function toResourcePolicyPrincipal(identifier) {
|
|
13
|
+
return identifier === "*"
|
|
14
|
+
? new StarPrincipal()
|
|
15
|
+
: new ArnPrincipal(identifier);
|
|
16
|
+
}
|
|
11
17
|
export class S3Bucket extends Bucket {
|
|
12
18
|
backupVaultTier;
|
|
13
19
|
constructor(scope, id, props = {}) {
|
|
14
|
-
const { websiteHosting, backupVaultTier, ...cdkProps } = props;
|
|
20
|
+
const { websiteHosting, backupVaultTier, resourcePolicyStatements, ...cdkProps } = props;
|
|
15
21
|
const isPublic = props.publicReadAccess === true || websiteHosting !== undefined;
|
|
16
22
|
const versioned = props.versioned ?? shouldAutoVersion(backupVaultTier);
|
|
17
23
|
const removalPolicy = props.removalPolicy ?? toRemovalPolicy(envAwareRemovalPolicyDefault());
|
|
@@ -42,6 +48,21 @@ export class S3Bucket extends Bucket {
|
|
|
42
48
|
: props.lifecycleRules
|
|
43
49
|
});
|
|
44
50
|
this.backupVaultTier = backupVaultTier;
|
|
51
|
+
for (const statement of resourcePolicyStatements ?? []) {
|
|
52
|
+
this.addToResourcePolicy(new PolicyStatement({
|
|
53
|
+
...(statement.sid !== undefined && { sid: statement.sid }),
|
|
54
|
+
effect: statement.effect === "Deny" ? Effect.DENY : Effect.ALLOW,
|
|
55
|
+
principals: statement.principals.map(toResourcePolicyPrincipal),
|
|
56
|
+
actions: statement.actions,
|
|
57
|
+
resources: statement.resources ?? [
|
|
58
|
+
this.bucketArn,
|
|
59
|
+
this.arnForObjects("*")
|
|
60
|
+
],
|
|
61
|
+
...(statement.conditions !== undefined && {
|
|
62
|
+
conditions: statement.conditions
|
|
63
|
+
})
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
45
66
|
if (props.autoDeleteObjects === true) {
|
|
46
67
|
Annotations.of(this).addWarningV2("@fjall/components-infrastructure:s3:autoDeleteObjectsIgnored", "autoDeleteObjects: true is ignored — the CDK auto-delete custom " +
|
|
47
68
|
"resource is retired (ADR D4.3). DESTROY buckets are emptied " +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@aws-sdk/client-organizations": "^3.1038.0",
|
|
66
|
-
"@fjall/generator": "^2.
|
|
67
|
-
"@fjall/util": "^2.
|
|
66
|
+
"@fjall/generator": "^2.16.0",
|
|
67
|
+
"@fjall/util": "^2.16.0",
|
|
68
68
|
"constructs": "^10.0.0",
|
|
69
69
|
"uuid": "^14.0.0"
|
|
70
70
|
},
|
|
@@ -79,5 +79,5 @@
|
|
|
79
79
|
"engines": {
|
|
80
80
|
"node": ">=18.0.0"
|
|
81
81
|
},
|
|
82
|
-
"gitHead": "
|
|
82
|
+
"gitHead": "2383b19f1e7db980ae603a6c75dca2c61b7a1d42"
|
|
83
83
|
}
|