@fjall/components-infrastructure 2.11.1 → 2.12.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/s3BlockPublicAccess.d.ts +22 -4
- package/dist/lib/config/aws/s3BlockPublicAccess.js +33 -13
- package/dist/lib/config/aws/scpPreset.js +6 -1
- package/dist/lib/patterns/aws/account.d.ts +7 -0
- package/dist/lib/patterns/aws/account.js +21 -5
- package/dist/lib/patterns/aws/organisation.d.ts +19 -8
- package/dist/lib/patterns/aws/organisation.js +23 -10
- package/dist/lib/patterns/aws/organisationFactory.js +2 -1
- package/dist/lib/resources/aws/compute/lambda.d.ts +11 -0
- package/dist/lib/resources/aws/compute/lambda.js +20 -0
- package/dist/lib/resources/aws/utilities/customResource.d.ts +4 -0
- package/dist/lib/resources/aws/utilities/customResource.js +4 -0
- package/dist/lib/utils/env.js +2 -2
- package/dist/lib/utils/getConfig.js +2 -2
- package/dist/lib/utils/orgConfigParser.js +8 -2
- package/package.json +4 -4
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
|
+
export interface S3BlockPublicAccessProps {
|
|
3
|
+
/**
|
|
4
|
+
* Account whose account-level S3 Block Public Access is managed. Taken as a
|
|
5
|
+
* prop rather than read from `Stack.of(this).account` — that is a synth token
|
|
6
|
+
* that never matches a provider-account id when looking up config.
|
|
7
|
+
*/
|
|
8
|
+
accountId: string;
|
|
9
|
+
}
|
|
2
10
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
11
|
+
* Manages account-level S3 Block Public Access via a Custom Resource.
|
|
12
|
+
*
|
|
13
|
+
* Off by default: Fjall observes-and-flags exposed buckets through the posture
|
|
14
|
+
* layer rather than hard-blocking at the account level. Set
|
|
15
|
+
* `s3BlockPublicAccess: "enforced"` on a provider account to switch all four
|
|
16
|
+
* flags on. The flags ride in the custom resource's properties (not hardcoded
|
|
17
|
+
* in the handler) so a config change reconciles on the next deploy. Stack
|
|
18
|
+
* deletion is a no-op — the account-level setting is left as-is.
|
|
19
|
+
*
|
|
20
|
+
* The handler runs under a fixed `/fjall/FjallS3BpaManager` role so the
|
|
21
|
+
* DenyS3PublicAccessChanges SCP can exempt its PutAccountPublicAccessBlock by
|
|
22
|
+
* ARN pattern. The role name is account-global, so this construct is created
|
|
23
|
+
* only in an account's home region (see Account's accountGlobals gate).
|
|
6
24
|
*/
|
|
7
25
|
export declare class S3BlockPublicAccess extends Construct {
|
|
8
|
-
constructor(scope: Construct, id: string);
|
|
26
|
+
constructor(scope: Construct, id: string, props: S3BlockPublicAccessProps);
|
|
9
27
|
}
|
|
@@ -1,23 +1,41 @@
|
|
|
1
1
|
import { Duration } from "aws-cdk-lib";
|
|
2
2
|
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
3
3
|
import { Runtime } from "aws-cdk-lib/aws-lambda";
|
|
4
|
-
import { Stack } from "aws-cdk-lib";
|
|
5
4
|
import { Construct } from "constructs";
|
|
6
5
|
import { CustomResource } from "../../resources/aws/utilities/customResource.js";
|
|
6
|
+
import { getConfig } from "../../utils/getConfig.js";
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Manages account-level S3 Block Public Access via a Custom Resource.
|
|
9
|
+
*
|
|
10
|
+
* Off by default: Fjall observes-and-flags exposed buckets through the posture
|
|
11
|
+
* layer rather than hard-blocking at the account level. Set
|
|
12
|
+
* `s3BlockPublicAccess: "enforced"` on a provider account to switch all four
|
|
13
|
+
* flags on. The flags ride in the custom resource's properties (not hardcoded
|
|
14
|
+
* in the handler) so a config change reconciles on the next deploy. Stack
|
|
15
|
+
* deletion is a no-op — the account-level setting is left as-is.
|
|
16
|
+
*
|
|
17
|
+
* The handler runs under a fixed `/fjall/FjallS3BpaManager` role so the
|
|
18
|
+
* DenyS3PublicAccessChanges SCP can exempt its PutAccountPublicAccessBlock by
|
|
19
|
+
* ARN pattern. The role name is account-global, so this construct is created
|
|
20
|
+
* only in an account's home region (see Account's accountGlobals gate).
|
|
11
21
|
*/
|
|
12
22
|
export class S3BlockPublicAccess extends Construct {
|
|
13
|
-
constructor(scope, id) {
|
|
23
|
+
constructor(scope, id, props) {
|
|
14
24
|
super(scope, id);
|
|
25
|
+
const account = getConfig().providerAccounts.find((pa) => pa.id === props.accountId);
|
|
26
|
+
const enforce = (account?.s3BlockPublicAccess ?? "off") === "enforced";
|
|
15
27
|
new CustomResource(this, "S3BlockPublicAccess", {
|
|
16
28
|
runtime: Runtime.NODEJS_22_X,
|
|
17
29
|
timeout: Duration.minutes(5),
|
|
18
|
-
lambdaDescription: "
|
|
30
|
+
lambdaDescription: "Manages S3 Block Public Access at account level",
|
|
31
|
+
rolePath: "/fjall/",
|
|
32
|
+
roleName: "FjallS3BpaManager",
|
|
19
33
|
properties: {
|
|
20
|
-
AccountId:
|
|
34
|
+
AccountId: props.accountId,
|
|
35
|
+
BlockPublicAcls: String(enforce),
|
|
36
|
+
IgnorePublicAcls: String(enforce),
|
|
37
|
+
BlockPublicPolicy: String(enforce),
|
|
38
|
+
RestrictPublicBuckets: String(enforce)
|
|
21
39
|
},
|
|
22
40
|
inlinePolicy: [
|
|
23
41
|
new PolicyStatement({
|
|
@@ -32,20 +50,22 @@ export class S3BlockPublicAccess extends Construct {
|
|
|
32
50
|
inlineCode: `
|
|
33
51
|
const { S3ControlClient, PutPublicAccessBlockCommand } = require('@aws-sdk/client-s3-control');
|
|
34
52
|
|
|
53
|
+
const toBool = (v) => v === 'true' || v === true;
|
|
54
|
+
|
|
35
55
|
exports.handler = async (event) => {
|
|
36
56
|
const physicalResourceId = event.PhysicalResourceId || event.LogicalResourceId || 's3-block-public-access';
|
|
37
57
|
if (event.RequestType === 'Delete') {
|
|
38
58
|
return { PhysicalResourceId: physicalResourceId };
|
|
39
59
|
}
|
|
40
|
-
const
|
|
60
|
+
const props = event.ResourceProperties;
|
|
41
61
|
const client = new S3ControlClient({});
|
|
42
62
|
await client.send(new PutPublicAccessBlockCommand({
|
|
43
|
-
AccountId:
|
|
63
|
+
AccountId: props.AccountId,
|
|
44
64
|
PublicAccessBlockConfiguration: {
|
|
45
|
-
BlockPublicAcls:
|
|
46
|
-
IgnorePublicAcls:
|
|
47
|
-
BlockPublicPolicy:
|
|
48
|
-
RestrictPublicBuckets:
|
|
65
|
+
BlockPublicAcls: toBool(props.BlockPublicAcls),
|
|
66
|
+
IgnorePublicAcls: toBool(props.IgnorePublicAcls),
|
|
67
|
+
BlockPublicPolicy: toBool(props.BlockPublicPolicy),
|
|
68
|
+
RestrictPublicBuckets: toBool(props.RestrictPublicBuckets)
|
|
49
69
|
}
|
|
50
70
|
}));
|
|
51
71
|
return { PhysicalResourceId: physicalResourceId };
|
|
@@ -5,7 +5,12 @@ import { OrganisationPolicy } from "../../resources/aws/organisation/organisatio
|
|
|
5
5
|
// default path.
|
|
6
6
|
const EXEMPT_ROLE_PATTERNS = [
|
|
7
7
|
"arn:aws:iam::*:role/fjall/FjallDeploy*",
|
|
8
|
-
"arn:aws:iam::*:role/OrganizationAccountAccessRole"
|
|
8
|
+
"arn:aws:iam::*:role/OrganizationAccountAccessRole",
|
|
9
|
+
// The account-level S3 Block Public Access manager Lambda re-applies BPA on
|
|
10
|
+
// every deploy; its PutAccountPublicAccessBlock must survive
|
|
11
|
+
// DenyS3PublicAccessChanges. Pathed under /fjall/ so a member admin cannot
|
|
12
|
+
// forge an exempt role at the default path.
|
|
13
|
+
"arn:aws:iam::*:role/fjall/FjallS3BpaManager*"
|
|
9
14
|
];
|
|
10
15
|
const SCP_BYTE_LIMIT = 5120;
|
|
11
16
|
const IAM_POLICY_VERSION = "2012-10-17";
|
|
@@ -27,6 +27,13 @@ export declare class Account extends Stack {
|
|
|
27
27
|
* independent, and Platform must take one but not the other.
|
|
28
28
|
*/
|
|
29
29
|
protected receivesDeployRole(): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Whether this account creates the default-ECR-image helper (the CodeBuild
|
|
32
|
+
* project that seeds a placeholder application image). App-hosting accounts
|
|
33
|
+
* need it; the organisation root runs no workloads and overrides to `false`.
|
|
34
|
+
* Deliberately separate from the OIDC/IPAM gates — the axes are independent.
|
|
35
|
+
*/
|
|
36
|
+
protected createsEcrDefaultImage(): boolean;
|
|
30
37
|
enableGuardDuty(props?: GuardDutyDetectorProps): GuardDutyDetector;
|
|
31
38
|
enableSecurityHub(props?: SecurityHubHubProps): SecurityHubHub;
|
|
32
39
|
enableConfigRecorder(props?: ConfigRecorderProps): ConfigRecorder;
|
|
@@ -75,10 +75,12 @@ export class Account extends Stack {
|
|
|
75
75
|
accountId: this.account,
|
|
76
76
|
region: this.resolvedRegion
|
|
77
77
|
});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
if (this.createsEcrDefaultImage()) {
|
|
79
|
+
new EcrDefaultImage(this, "EcrDefaultImage", {
|
|
80
|
+
region: this.resolvedRegion,
|
|
81
|
+
accountId: this.account
|
|
82
|
+
});
|
|
83
|
+
}
|
|
82
84
|
const environment = config.environment ?? "unknown";
|
|
83
85
|
if (config.disasterRecoveryRegion) {
|
|
84
86
|
const isComplianceAccount = environment === "compliance";
|
|
@@ -95,7 +97,12 @@ export class Account extends Stack {
|
|
|
95
97
|
exportName: "Environment",
|
|
96
98
|
description: "Environment type for this account (e.g., production, staging, development)"
|
|
97
99
|
});
|
|
98
|
-
|
|
100
|
+
// Account-level S3 Block Public Access is account-global and runs under a
|
|
101
|
+
// fixed-name role, so (like the OIDC/Monitoring/Audit globals above) it is
|
|
102
|
+
// created only in the home region to avoid a cross-region role collision.
|
|
103
|
+
if (!accountGlobalsConfigured) {
|
|
104
|
+
new S3BlockPublicAccess(this, "S3BlockPublicAccess", { accountId });
|
|
105
|
+
}
|
|
99
106
|
new EbsDefaultEncryption(this, "EbsDefaultEncryption");
|
|
100
107
|
}
|
|
101
108
|
/**
|
|
@@ -108,6 +115,15 @@ export class Account extends Stack {
|
|
|
108
115
|
receivesDeployRole() {
|
|
109
116
|
return this.constructor === Account;
|
|
110
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Whether this account creates the default-ECR-image helper (the CodeBuild
|
|
120
|
+
* project that seeds a placeholder application image). App-hosting accounts
|
|
121
|
+
* need it; the organisation root runs no workloads and overrides to `false`.
|
|
122
|
+
* Deliberately separate from the OIDC/IPAM gates — the axes are independent.
|
|
123
|
+
*/
|
|
124
|
+
createsEcrDefaultImage() {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
111
127
|
enableGuardDuty(props) {
|
|
112
128
|
return new GuardDutyDetector(this, "GuardDuty", props);
|
|
113
129
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { type Environment
|
|
1
|
+
import { type Environment } from "aws-cdk-lib";
|
|
2
|
+
import { type Construct } from "constructs";
|
|
3
|
+
import { Account, type AccountProps } from "./account.js";
|
|
2
4
|
import { type CustomPermissionSets } from "../../config/aws/identityCenter.js";
|
|
3
5
|
import { ScpPreset } from "../../config/aws/scpPreset.js";
|
|
4
6
|
import type { ScpPresetProps } from "../../config/aws/scpPreset.js";
|
|
5
7
|
import { OrganisationResource } from "../../resources/aws/organisation/index.js";
|
|
6
|
-
type ExtendedStackProps = Omit<StackProps, "env"> & {
|
|
7
|
-
env?: Required<Pick<Environment, "region" | "account">> & Partial<Omit<Environment, "region" | "account">>;
|
|
8
|
-
};
|
|
9
8
|
export type AccountsConfig = {
|
|
10
9
|
readonly [key: string]: readonly string[] | string | AccountsConfig;
|
|
11
10
|
};
|
|
12
|
-
export interface OrganisationProps extends
|
|
11
|
+
export interface OrganisationProps extends AccountProps {
|
|
13
12
|
organisationName: string;
|
|
14
13
|
accounts: AccountsConfig;
|
|
15
14
|
orgEmail: string;
|
|
16
15
|
accountIds?: Record<string, string>;
|
|
17
16
|
identityCenter?: boolean;
|
|
18
17
|
allowedRegions?: string[];
|
|
18
|
+
env?: Required<Pick<Environment, "region" | "account">> & Partial<Omit<Environment, "region" | "account">>;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Organisation CDK stack.
|
|
@@ -27,14 +27,26 @@ export interface OrganisationProps extends ExtendedStackProps {
|
|
|
27
27
|
* - Identity Centre configuration
|
|
28
28
|
* - Cost allocation tag auto-activation (daily Lambda)
|
|
29
29
|
*/
|
|
30
|
-
export declare class Organisation extends
|
|
30
|
+
export declare class Organisation extends Account {
|
|
31
31
|
readonly organisationType: "organisation";
|
|
32
32
|
private org;
|
|
33
33
|
private accountRefs;
|
|
34
34
|
private accountMap;
|
|
35
35
|
private accountsConfig;
|
|
36
36
|
private identityCenter?;
|
|
37
|
-
constructor(id: string, props: OrganisationProps);
|
|
37
|
+
constructor(scope: Construct, id: string, props: OrganisationProps);
|
|
38
|
+
/**
|
|
39
|
+
* The organisation root's OIDC deploy connector is owned by the customer-run
|
|
40
|
+
* Quick-Create CloudFormation stack, never the inherited Account connector.
|
|
41
|
+
* Returning `false` is also the short-circuit first clause of Account's OIDC
|
|
42
|
+
* gate, so no `OidcConnector` is synthesised on the root stack.
|
|
43
|
+
*/
|
|
44
|
+
protected receivesDeployRole(): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The organisation root runs no application workloads, so it needs no
|
|
47
|
+
* default-ECR-image helper.
|
|
48
|
+
*/
|
|
49
|
+
protected createsEcrDefaultImage(): boolean;
|
|
38
50
|
private createAccountReferences;
|
|
39
51
|
private setupOrganisationFeatures;
|
|
40
52
|
private setupCostAllocationTagActivator;
|
|
@@ -45,4 +57,3 @@ export declare class Organisation extends Stack {
|
|
|
45
57
|
getAccounts(): Record<string, string>;
|
|
46
58
|
enableScps(props: ScpPresetProps): ScpPreset;
|
|
47
59
|
}
|
|
48
|
-
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Stack } from "aws-cdk-lib";
|
|
2
1
|
import App from "../../app.js";
|
|
2
|
+
import { Account } from "./account.js";
|
|
3
3
|
import { IdentityCenter } from "../../config/aws/identityCenter.js";
|
|
4
4
|
import { ScpPreset } from "../../config/aws/scpPreset.js";
|
|
5
5
|
import { OrganisationResource, OrganisationAccount, CostAllocationTagActivator } from "../../resources/aws/organisation/index.js";
|
|
@@ -17,23 +17,20 @@ import { extractAccountNames } from "../../utils/accountsUtils.js";
|
|
|
17
17
|
* - Identity Centre configuration
|
|
18
18
|
* - Cost allocation tag auto-activation (daily Lambda)
|
|
19
19
|
*/
|
|
20
|
-
export class Organisation extends
|
|
20
|
+
export class Organisation extends Account {
|
|
21
21
|
organisationType = "organisation";
|
|
22
22
|
org;
|
|
23
23
|
accountRefs = [];
|
|
24
24
|
accountMap;
|
|
25
25
|
accountsConfig;
|
|
26
26
|
identityCenter;
|
|
27
|
-
constructor(id, props) {
|
|
27
|
+
constructor(scope, id, props) {
|
|
28
28
|
const config = getConfig();
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
account
|
|
32
|
-
};
|
|
33
|
-
if (!env.account) {
|
|
34
|
-
throw new Error("Organisation requires an account ID. Provide it via env.account or ensure CDK context includes accountId.");
|
|
29
|
+
const accountId = props.accountId ?? props.env?.account ?? config.accountId;
|
|
30
|
+
if (!accountId) {
|
|
31
|
+
throw new Error("Organisation requires an account ID. Provide it via env.account, accountId, or ensure CDK context includes accountId.");
|
|
35
32
|
}
|
|
36
|
-
super(
|
|
33
|
+
super(scope, id, { ...props, accountId });
|
|
37
34
|
// Normalise account map keys to lowercase for case-insensitive lookups.
|
|
38
35
|
// providerAccounts stores names as lowercase; ACCOUNTS config uses PascalCase.
|
|
39
36
|
const rawMap = props.accountIds ?? config.accountIds ?? {};
|
|
@@ -57,6 +54,22 @@ export class Organisation extends Stack {
|
|
|
57
54
|
this.createAccountReferences(props);
|
|
58
55
|
this.setupOrganisationFeatures(props.identityCenter ?? true, managementAccountId);
|
|
59
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* The organisation root's OIDC deploy connector is owned by the customer-run
|
|
59
|
+
* Quick-Create CloudFormation stack, never the inherited Account connector.
|
|
60
|
+
* Returning `false` is also the short-circuit first clause of Account's OIDC
|
|
61
|
+
* gate, so no `OidcConnector` is synthesised on the root stack.
|
|
62
|
+
*/
|
|
63
|
+
receivesDeployRole() {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The organisation root runs no application workloads, so it needs no
|
|
68
|
+
* default-ECR-image helper.
|
|
69
|
+
*/
|
|
70
|
+
createsEcrDefaultImage() {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
60
73
|
createAccountReferences(props) {
|
|
61
74
|
const allNames = extractAccountNames(props.accounts);
|
|
62
75
|
for (const accountName of allNames) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import App from "../../app.js";
|
|
1
2
|
import { Organisation } from "./organisation.js";
|
|
2
3
|
import { Platform } from "./platform.js";
|
|
3
4
|
import { Account } from "./account.js";
|
|
@@ -5,7 +6,7 @@ export class OrganisationFactory {
|
|
|
5
6
|
static build(id, props) {
|
|
6
7
|
switch (props.type) {
|
|
7
8
|
case "organisation":
|
|
8
|
-
return new Organisation(id, props);
|
|
9
|
+
return new Organisation(App.getInstance(), id, props);
|
|
9
10
|
case "platform":
|
|
10
11
|
return (scope) => new Platform(scope, id, props);
|
|
11
12
|
case "account":
|
|
@@ -15,6 +15,17 @@ export interface LambdaFunctionProps {
|
|
|
15
15
|
handler: string;
|
|
16
16
|
lambdaDescription?: string;
|
|
17
17
|
roleDescription?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Fixed IAM path for the auto-generated execution role (e.g. "/fjall/").
|
|
20
|
+
* Used with roleName to produce an SCP-exemptable ARN.
|
|
21
|
+
*/
|
|
22
|
+
rolePath?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Fixed name for the auto-generated execution role. Account-global — only set
|
|
25
|
+
* on a Lambda confined to one (account, region) stack, else regional
|
|
26
|
+
* duplicates collide on the role name.
|
|
27
|
+
*/
|
|
28
|
+
roleName?: string;
|
|
18
29
|
runtime: Runtime;
|
|
19
30
|
/** Lambda CPU architecture. Default: x86_64 */
|
|
20
31
|
architecture?: Architecture;
|
|
@@ -31,6 +31,24 @@ function applyRoleDescription(fn, description) {
|
|
|
31
31
|
if (cfnRole !== undefined)
|
|
32
32
|
cfnRole.description = description;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Pin a fixed IAM path and/or name onto CDK's auto-generated Lambda execution
|
|
36
|
+
* role. Reaches the L1 CfnRole (FunctionProps exposes neither) so a stable,
|
|
37
|
+
* predictable ARN can be matched by an SCP exemption pattern. A fixed name is
|
|
38
|
+
* account-global, so only set one on a Lambda that lives in a single
|
|
39
|
+
* (account, region) stack — never one duplicated across regions.
|
|
40
|
+
*/
|
|
41
|
+
function applyRolePathAndName(fn, rolePath, roleName) {
|
|
42
|
+
if (rolePath === undefined && roleName === undefined)
|
|
43
|
+
return;
|
|
44
|
+
const cfnRole = fn.role?.node.defaultChild;
|
|
45
|
+
if (cfnRole === undefined)
|
|
46
|
+
return;
|
|
47
|
+
if (rolePath !== undefined)
|
|
48
|
+
cfnRole.path = rolePath;
|
|
49
|
+
if (roleName !== undefined)
|
|
50
|
+
cfnRole.roleName = roleName;
|
|
51
|
+
}
|
|
34
52
|
/**
|
|
35
53
|
* AWS Parameters and Secrets Lambda Extension configuration.
|
|
36
54
|
* @see https://docs.aws.amazon.com/systems-manager/latest/userguide/ps-integration-lambda-extensions.html
|
|
@@ -61,6 +79,7 @@ export class SingletonFunction extends singletonFunction {
|
|
|
61
79
|
});
|
|
62
80
|
addPoliciesToRole(this, props.inlinePolicy);
|
|
63
81
|
applyRoleDescription(this, props.roleDescription);
|
|
82
|
+
applyRolePathAndName(this, props.rolePath, props.roleName);
|
|
64
83
|
}
|
|
65
84
|
/**
|
|
66
85
|
* The Lambda's execution role (auto-generated by CDK)
|
|
@@ -91,6 +110,7 @@ export class LambdaFunction extends Function {
|
|
|
91
110
|
});
|
|
92
111
|
addPoliciesToRole(this, props.inlinePolicy);
|
|
93
112
|
applyRoleDescription(this, props.roleDescription);
|
|
113
|
+
applyRolePathAndName(this, props.rolePath, props.roleName);
|
|
94
114
|
this.addSecretsSupport(props.secrets, props.ssmSecretsPath, props.secretsImport, props.appName, props.functionName, props.architecture);
|
|
95
115
|
// Sanitise id for CloudFormation output keys (must be alphanumeric)
|
|
96
116
|
const outputName = toPascalCase(id);
|
|
@@ -7,6 +7,10 @@ interface CustomResourceProps {
|
|
|
7
7
|
runtime: Runtime;
|
|
8
8
|
roleDescription?: string;
|
|
9
9
|
lambdaDescription?: string;
|
|
10
|
+
/** Fixed IAM path for the handler's execution role (e.g. "/fjall/"). */
|
|
11
|
+
rolePath?: string;
|
|
12
|
+
/** Fixed name for the handler's execution role (account-global — see LambdaFunctionProps). */
|
|
13
|
+
roleName?: string;
|
|
10
14
|
inlinePolicy: PolicyStatement[];
|
|
11
15
|
properties?: {
|
|
12
16
|
[key: string]: string;
|
|
@@ -39,6 +39,8 @@ export class CustomResource extends Construct {
|
|
|
39
39
|
timeout,
|
|
40
40
|
lambdaDescription: props.lambdaDescription ?? `${id} lambda`,
|
|
41
41
|
roleDescription: props.roleDescription ?? `${id} custom resource lambda`,
|
|
42
|
+
rolePath: props.rolePath,
|
|
43
|
+
roleName: props.roleName,
|
|
42
44
|
inlinePolicy: props.inlinePolicy
|
|
43
45
|
})
|
|
44
46
|
: new SingletonFunction(this, `${id}Lambda`, {
|
|
@@ -47,6 +49,8 @@ export class CustomResource extends Construct {
|
|
|
47
49
|
runtime: props.runtime,
|
|
48
50
|
lambdaDescription: props.lambdaDescription ?? `${id} lambda`,
|
|
49
51
|
roleDescription: props.roleDescription ?? `${id} custom resource lambda`,
|
|
52
|
+
rolePath: props.rolePath,
|
|
53
|
+
roleName: props.roleName,
|
|
50
54
|
inlinePolicy: props.inlinePolicy
|
|
51
55
|
});
|
|
52
56
|
const provider = new Provider(this, `${id}Provider`, {
|
package/dist/lib/utils/env.js
CHANGED
|
@@ -99,12 +99,12 @@ function getProviderAccountsFromContext() {
|
|
|
99
99
|
*/
|
|
100
100
|
function resolveEnvironmentFromAccountId(accountId) {
|
|
101
101
|
const accounts = getProviderAccountsFromContext();
|
|
102
|
-
return accounts.find((pa) => pa.id === accountId)?.environment;
|
|
102
|
+
return accounts.find((pa) => pa.id === accountId)?.environment ?? undefined;
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
105
105
|
* Look up environment from account name via orgConfig CDK context.
|
|
106
106
|
*/
|
|
107
107
|
function resolveEnvironmentFromAccountName(accountName) {
|
|
108
108
|
const accounts = getProviderAccountsFromContext();
|
|
109
|
-
return accounts.find((pa) => pa.name === accountName)?.environment;
|
|
109
|
+
return (accounts.find((pa) => pa.name === accountName)?.environment ?? undefined);
|
|
110
110
|
}
|
|
@@ -63,7 +63,7 @@ export function getConfig(accountName) {
|
|
|
63
63
|
if (providerAccount) {
|
|
64
64
|
config.accountId = providerAccount.id;
|
|
65
65
|
config.accountName = providerAccount.name;
|
|
66
|
-
config.environment = providerAccount.environment;
|
|
66
|
+
config.environment = providerAccount.environment ?? "unknown";
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
// If we still don't have an account name - try to retrieve accountId from context
|
|
@@ -75,7 +75,7 @@ export function getConfig(accountName) {
|
|
|
75
75
|
const providerAccount = providerAccounts.find((pa) => pa.id === accountId);
|
|
76
76
|
if (providerAccount) {
|
|
77
77
|
config.accountName = providerAccount.name;
|
|
78
|
-
config.environment = providerAccount.environment;
|
|
78
|
+
config.environment = providerAccount.environment ?? "unknown";
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { VAULT_LOCK_MODES } from "@fjall/util/config";
|
|
1
|
+
import { VAULT_LOCK_MODES, S3_BPA_MODES } from "@fjall/util/config";
|
|
2
2
|
import { maskSensitiveOutput } from "@fjall/util";
|
|
3
3
|
import { FjallLogger } from "./validationLogger.js";
|
|
4
4
|
/**
|
|
@@ -24,7 +24,10 @@ export function parseOrgConfig(raw) {
|
|
|
24
24
|
item !== null &&
|
|
25
25
|
typeof item.id === "string" &&
|
|
26
26
|
typeof item.name === "string" &&
|
|
27
|
-
|
|
27
|
+
// null environment (structural accounts) must survive — rejecting it drops the account at synth.
|
|
28
|
+
(typeof item.environment ===
|
|
29
|
+
"string" ||
|
|
30
|
+
item.environment === null) &&
|
|
28
31
|
(item.managed === undefined ||
|
|
29
32
|
typeof item.managed === "boolean") &&
|
|
30
33
|
(item.oidcRoleArn === undefined ||
|
|
@@ -32,6 +35,9 @@ export function parseOrgConfig(raw) {
|
|
|
32
35
|
"string") &&
|
|
33
36
|
(item.vaultLock === undefined ||
|
|
34
37
|
VAULT_LOCK_MODES.includes(item.vaultLock)) &&
|
|
38
|
+
(item.s3BlockPublicAccess ===
|
|
39
|
+
undefined ||
|
|
40
|
+
S3_BPA_MODES.includes(item.s3BlockPublicAccess)) &&
|
|
35
41
|
(item.acknowledgeImmutableVaultLock ===
|
|
36
42
|
undefined ||
|
|
37
43
|
typeof item
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.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.12.0",
|
|
67
|
+
"@fjall/util": "^2.12.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": "dca39a47da956d3d94c689dd782fe285d711d25e"
|
|
83
83
|
}
|