@fjall/components-infrastructure 2.23.1 → 2.25.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/identityCenter.d.ts +20 -8
- package/dist/lib/config/aws/identityCenter.js +194 -85
- package/dist/lib/config/aws/identityCentreConfig.d.ts +147 -0
- package/dist/lib/config/aws/identityCentreConfig.js +338 -0
- package/dist/lib/config/aws/index.d.ts +1 -1
- package/dist/lib/config/aws/index.js +1 -1
- package/dist/lib/config/aws/oidcConnector.d.ts +9 -0
- package/dist/lib/config/aws/oidcConnector.js +17 -1
- package/dist/lib/lambda-assets/identity-store-user/asset/index.js +198 -0
- package/dist/lib/lambda-assets/identity-store-user/asset/package.json +4 -0
- package/dist/lib/patterns/aws/account.d.ts +11 -0
- package/dist/lib/patterns/aws/account.js +6 -1
- package/dist/lib/patterns/aws/computeEcsTypes.d.ts +3 -2
- package/dist/lib/patterns/aws/organisation.d.ts +9 -5
- package/dist/lib/patterns/aws/organisation.js +24 -27
- package/dist/lib/resources/aws/compute/ecsTypes.d.ts +1 -1
- package/dist/lib/resources/aws/database/rdsAurora.d.ts +1 -0
- package/dist/lib/resources/aws/database/rdsAurora.js +34 -6
- package/dist/lib/resources/aws/database/rdsAuroraGlobal.d.ts +1 -1
- package/dist/lib/resources/aws/database/rdsDefaults.d.ts +0 -2
- package/dist/lib/resources/aws/database/rdsDefaults.js +0 -2
- package/dist/lib/resources/aws/database/rdsInstance.d.ts +1 -0
- package/dist/lib/resources/aws/database/rdsInstance.js +24 -3
- package/dist/lib/resources/aws/iam/identityCenter/index.d.ts +1 -0
- package/dist/lib/resources/aws/iam/identityCenter/index.js +1 -0
- package/dist/lib/resources/aws/iam/identityCenter/permissionSet.d.ts +4 -0
- package/dist/lib/resources/aws/iam/identityCenter/permissionSet.js +12 -0
- package/dist/lib/resources/aws/iam/identityCenter/user.d.ts +35 -0
- package/dist/lib/resources/aws/iam/identityCenter/user.js +85 -0
- package/package.json +6 -5
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Environment } from "aws-cdk-lib";
|
|
2
2
|
import { type Construct } from "constructs";
|
|
3
3
|
import { Account, type AccountProps } from "./account.js";
|
|
4
|
-
import { type
|
|
4
|
+
import { type IdentityCentreConfig } from "../../config/aws/identityCentreConfig.js";
|
|
5
5
|
import { ScpPreset } from "../../config/aws/scpPreset.js";
|
|
6
6
|
import type { ScpPresetProps } from "../../config/aws/scpPreset.js";
|
|
7
7
|
import { OrganisationResource } from "../../resources/aws/organisation/index.js";
|
|
@@ -13,7 +13,13 @@ export interface OrganisationProps extends AccountProps {
|
|
|
13
13
|
accounts: AccountsConfig;
|
|
14
14
|
orgEmail: string;
|
|
15
15
|
accountIds?: Record<string, string>;
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Identity Centre configuration: groups, permission sets, memberships and
|
|
18
|
+
* (source permitting) users, all declared in code. `false` disables the
|
|
19
|
+
* Identity Centre entirely; omitted = the three built-in permission sets
|
|
20
|
+
* with no members (inert defaults).
|
|
21
|
+
*/
|
|
22
|
+
identityCentre?: IdentityCentreConfig | false;
|
|
17
23
|
allowedRegions?: string[];
|
|
18
24
|
env?: Required<Pick<Environment, "region" | "account">> & Partial<Omit<Environment, "region" | "account">>;
|
|
19
25
|
}
|
|
@@ -59,9 +65,7 @@ export declare class Organisation extends Account {
|
|
|
59
65
|
private createAccountReferences;
|
|
60
66
|
private setupOrganisationFeatures;
|
|
61
67
|
private setupCostAllocationTagActivator;
|
|
62
|
-
private
|
|
63
|
-
declarePermissionSets(customs: CustomPermissionSets): void;
|
|
64
|
-
assignGroupMembers(members: Record<string, string[]>): void;
|
|
68
|
+
private setupIdentityCentre;
|
|
65
69
|
getOrganisation(): OrganisationResource;
|
|
66
70
|
getAccounts(): Record<string, string>;
|
|
67
71
|
enableScps(props: ScpPresetProps): ScpPreset;
|
|
@@ -28,6 +28,12 @@ export class Organisation extends Account {
|
|
|
28
28
|
accountsConfig;
|
|
29
29
|
identityCenter;
|
|
30
30
|
constructor(scope, id, props) {
|
|
31
|
+
// Customer synth runs infrastructure.ts through tsx, which never
|
|
32
|
+
// typechecks — a removed prop is silently ignored, so the legacy key
|
|
33
|
+
// (e.g. `identityCenter: false`) would re-enable the Identity Centre.
|
|
34
|
+
if ("identityCenter" in props) {
|
|
35
|
+
throw new Error("OrganisationProps.identityCenter has been replaced by identityCentre (IdentityCentreConfig | false). Rename the key — identityCentre: false disables the Identity Centre; omitting it keeps the built-in permission sets.");
|
|
36
|
+
}
|
|
31
37
|
const config = getConfig();
|
|
32
38
|
const accountId = props.accountId ?? props.env?.account ?? config.accountId;
|
|
33
39
|
if (!accountId) {
|
|
@@ -88,7 +94,7 @@ export class Organisation extends Account {
|
|
|
88
94
|
}
|
|
89
95
|
}
|
|
90
96
|
this.createAccountReferences(props);
|
|
91
|
-
this.setupOrganisationFeatures(props.
|
|
97
|
+
this.setupOrganisationFeatures(props.identityCentre, managementAccountId);
|
|
92
98
|
}
|
|
93
99
|
/**
|
|
94
100
|
* The organisation root's OIDC deploy connector is owned by the customer-run
|
|
@@ -133,8 +139,8 @@ export class Organisation extends Account {
|
|
|
133
139
|
this.accountRefs.push(ref);
|
|
134
140
|
}
|
|
135
141
|
}
|
|
136
|
-
setupOrganisationFeatures(
|
|
137
|
-
this.
|
|
142
|
+
setupOrganisationFeatures(identityCentre, managementAccountId) {
|
|
143
|
+
this.setupIdentityCentre(identityCentre, managementAccountId);
|
|
138
144
|
// Home region only: Cost Explorer is a global service (single us-east-1
|
|
139
145
|
// endpoint), so an org-region cascade stack would otherwise synthesise a
|
|
140
146
|
// redundant daily activator Lambda per region against the same global
|
|
@@ -157,32 +163,23 @@ export class Organisation extends Account {
|
|
|
157
163
|
appName: App.getInstance().getName()
|
|
158
164
|
});
|
|
159
165
|
}
|
|
160
|
-
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
// IAM users should have direct access to the organisation account.
|
|
164
|
-
const ssoAccounts = {};
|
|
165
|
-
for (const [name, id] of Object.entries(this.accountMap)) {
|
|
166
|
-
if (id !== managementAccountId) {
|
|
167
|
-
ssoAccounts[name] = id;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
this.identityCenter = new IdentityCenter(this, "IdentityCenter", {
|
|
171
|
-
accounts: ssoAccounts
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
declarePermissionSets(customs) {
|
|
176
|
-
if (this.identityCenter === undefined) {
|
|
177
|
-
throw new Error("Identity Center is not enabled. Pass identityCenter: true to OrganisationFactory.");
|
|
166
|
+
setupIdentityCentre(identityCentre, managementAccountId) {
|
|
167
|
+
if (identityCentre === false) {
|
|
168
|
+
return;
|
|
178
169
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
170
|
+
// Exclude the management account from default SSO assignments — a
|
|
171
|
+
// permission set reaches it only via includeManagementAccount: true.
|
|
172
|
+
const ssoAccounts = {};
|
|
173
|
+
for (const [name, id] of Object.entries(this.accountMap)) {
|
|
174
|
+
if (id !== managementAccountId) {
|
|
175
|
+
ssoAccounts[name] = id;
|
|
176
|
+
}
|
|
184
177
|
}
|
|
185
|
-
this.identityCenter
|
|
178
|
+
this.identityCenter = new IdentityCenter(this, "IdentityCenter", {
|
|
179
|
+
accounts: ssoAccounts,
|
|
180
|
+
managementAccountId,
|
|
181
|
+
...(identityCentre !== undefined && { config: identityCentre })
|
|
182
|
+
});
|
|
186
183
|
}
|
|
187
184
|
getOrganisation() {
|
|
188
185
|
return this.org;
|
|
@@ -427,7 +427,7 @@ export interface EcsServiceProps {
|
|
|
427
427
|
ssmSecretsPath?: string;
|
|
428
428
|
/**
|
|
429
429
|
* Dockerfile build configuration for this service. When `target` is set,
|
|
430
|
-
* the image tag
|
|
430
|
+
* the content-hash image tag becomes `<service>-<target>-sha-<12 hex>`.
|
|
431
431
|
* Mutually exclusive with `image` (pre-built URI).
|
|
432
432
|
*/
|
|
433
433
|
docker?: DockerBuild;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Duration, RemovalPolicy } from "aws-cdk-lib";
|
|
1
|
+
import { Annotations, Duration, RemovalPolicy } from "aws-cdk-lib";
|
|
2
|
+
import { ENGINE_DEFAULT_DATABASE_PORTS } from "@fjall/generator";
|
|
2
3
|
import { Connections, Peer, Port, SubnetType } from "aws-cdk-lib/aws-ec2";
|
|
3
4
|
import { AuroraPostgresEngineVersion, CaCertificate, ClusterInstance, Credentials, DatabaseCluster, DatabaseClusterFromSnapshot, DatabaseClusterEngine, DatabaseProxy, ParameterGroup, ProxyTarget, SnapshotCredentials } from "aws-cdk-lib/aws-rds";
|
|
4
5
|
import { Construct } from "constructs";
|
|
@@ -14,6 +15,7 @@ export class RdsAurora extends Construct {
|
|
|
14
15
|
connections;
|
|
15
16
|
constructId;
|
|
16
17
|
port;
|
|
18
|
+
clientPort;
|
|
17
19
|
engineConfig;
|
|
18
20
|
databaseCluster;
|
|
19
21
|
databaseProxy;
|
|
@@ -23,10 +25,16 @@ export class RdsAurora extends Construct {
|
|
|
23
25
|
constructor(scope, id, props) {
|
|
24
26
|
super(scope, id);
|
|
25
27
|
this.constructId = id;
|
|
26
|
-
this.port = props.port ?? RDS_DEFAULTS.DEFAULT_PORT;
|
|
27
28
|
this.databaseNameValue = props.databaseName ?? id;
|
|
28
29
|
// PostgreSQL fallback for direct usage - ensure engine and engineConfig match
|
|
29
30
|
this.engineConfig = props.engineConfig ?? DEFAULT_POSTGRES_ENGINE_CONFIG;
|
|
31
|
+
const engineDefaultPort = ENGINE_DEFAULT_DATABASE_PORTS[this.engineConfig.family];
|
|
32
|
+
this.port = props.port ?? engineDefaultPort;
|
|
33
|
+
// RDS Proxy listens exclusively on the engine-default port (CreateDBProxy
|
|
34
|
+
// has no port parameter), so the client-facing surface must advertise it
|
|
35
|
+
// whenever a proxy fronts the cluster.
|
|
36
|
+
const proxyEnabled = props.proxy !== undefined && props.proxy !== false;
|
|
37
|
+
this.clientPort = proxyEnabled ? engineDefaultPort : this.port;
|
|
30
38
|
const { piEnabled, piConfig } = resolveDatabaseInsights(props.databaseInsights);
|
|
31
39
|
// Username priority: snapshotUsername > credentials.username > engine default
|
|
32
40
|
const username = props.snapshotIdentifier && props.snapshotUsername
|
|
@@ -158,7 +166,6 @@ export class RdsAurora extends Construct {
|
|
|
158
166
|
this.cfnCluster.masterUserPassword = undefined;
|
|
159
167
|
this.cfnCluster.databaseName = undefined;
|
|
160
168
|
}
|
|
161
|
-
const proxyEnabled = props.proxy !== undefined && props.proxy !== false;
|
|
162
169
|
if (proxyEnabled) {
|
|
163
170
|
this.addProxy(props, clusterSecurityGroup);
|
|
164
171
|
}
|
|
@@ -214,18 +221,26 @@ export class RdsAurora extends Construct {
|
|
|
214
221
|
});
|
|
215
222
|
});
|
|
216
223
|
}
|
|
217
|
-
addProxy(props,
|
|
224
|
+
addProxy(props, clusterSecurityGroup) {
|
|
218
225
|
if (!props.proxy)
|
|
219
226
|
return;
|
|
220
227
|
const proxyConfig = props.proxy;
|
|
221
228
|
const vpcSubnets = proxyConfig.vpcSubnets ?? {
|
|
222
229
|
subnetType: SubnetType.PRIVATE_WITH_EGRESS
|
|
223
230
|
};
|
|
231
|
+
const proxySecurityGroup = new SecurityGroup(this, `${this.databaseNameValue}ProxySecurityGroup`, {
|
|
232
|
+
vpc: props.vpc,
|
|
233
|
+
description: `Security group for RDS Proxy ${this.databaseNameValue}`
|
|
234
|
+
});
|
|
235
|
+
clusterSecurityGroup.addIngressRule(proxySecurityGroup, Port.tcp(this.port), "Allow RDS Proxy to connect to Aurora cluster");
|
|
236
|
+
if (props.allowVpcAccess) {
|
|
237
|
+
proxySecurityGroup.addIngressRule(Peer.ipv4(props.vpc.vpcCidrBlock), Port.tcp(this.clientPort), "Allow access from VPC CIDR");
|
|
238
|
+
}
|
|
224
239
|
this.databaseProxy = new DatabaseProxy(this, `${this.databaseNameValue}DatabaseProxy`, {
|
|
225
240
|
dbProxyName: ResourceNaming.proxyName(this.constructId),
|
|
226
241
|
proxyTarget: ProxyTarget.fromCluster(this.databaseCluster),
|
|
227
242
|
secrets: [this.databaseCredentials.secret],
|
|
228
|
-
securityGroups: [
|
|
243
|
+
securityGroups: [proxySecurityGroup],
|
|
229
244
|
vpc: props.vpc,
|
|
230
245
|
vpcSubnets,
|
|
231
246
|
requireTLS: proxyConfig.requireTLS ?? true,
|
|
@@ -235,6 +250,19 @@ export class RdsAurora extends Construct {
|
|
|
235
250
|
maxConnectionsPercent: proxyConfig.maxConnections,
|
|
236
251
|
maxIdleConnectionsPercent: proxyConfig.maxIdleConnections
|
|
237
252
|
});
|
|
253
|
+
// The proxy is now the client-facing surface: grantConnect /
|
|
254
|
+
// allowDefaultPortFrom must open the PROXY security group on the proxy's
|
|
255
|
+
// fixed engine-default listener port, not the cluster SG on the DB port.
|
|
256
|
+
this.connections = new Connections({
|
|
257
|
+
securityGroups: [proxySecurityGroup],
|
|
258
|
+
defaultPort: Port.tcp(this.clientPort)
|
|
259
|
+
});
|
|
260
|
+
if (this.port !== this.clientPort) {
|
|
261
|
+
Annotations.of(this).addWarning(`Database '${this.databaseNameValue}' listens on port ${this.port}, but its ` +
|
|
262
|
+
`RDS Proxy accepts client connections only on ${this.clientPort} (the engine ` +
|
|
263
|
+
`default — AWS does not allow configuring the proxy listener port). Fjall ` +
|
|
264
|
+
`advertises ${this.clientPort}; direct-to-database access uses ${this.port}.`);
|
|
265
|
+
}
|
|
238
266
|
addProxyCfnOutput(this, this.constructId, this.databaseNameValue, this.databaseProxy);
|
|
239
267
|
}
|
|
240
268
|
addSecretRotation(props) {
|
|
@@ -257,7 +285,7 @@ export class RdsAurora extends Construct {
|
|
|
257
285
|
return this.databaseCluster.clusterEndpoint.hostname;
|
|
258
286
|
}
|
|
259
287
|
getHostPort() {
|
|
260
|
-
return String(this.
|
|
288
|
+
return String(this.clientPort);
|
|
261
289
|
}
|
|
262
290
|
getCredentials() {
|
|
263
291
|
return this.databaseCredentials;
|
|
@@ -43,7 +43,7 @@ interface RdsAuroraGlobalProps {
|
|
|
43
43
|
monitoringInterval?: Duration;
|
|
44
44
|
/** Preferred maintenance window. Default: "Sat:12:30-Sat:20:30" */
|
|
45
45
|
preferredMaintenanceWindow?: string;
|
|
46
|
-
/** Database port. Default:
|
|
46
|
+
/** Database port. Default: engine default (5432 postgresql / 3306 mysql) */
|
|
47
47
|
port?: number;
|
|
48
48
|
databaseInsights?: DatabaseInsightsConfig | false;
|
|
49
49
|
deletionProtection?: boolean;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { Duration } from "aws-cdk-lib";
|
|
2
2
|
export declare const RDS_DEFAULTS: Readonly<{
|
|
3
|
-
/** Default database port: FJALL on phone keypad */
|
|
4
|
-
readonly DEFAULT_PORT: 35255;
|
|
5
3
|
/** Saturday window in UTC — minimises weekday impact for scheduled maintenance */
|
|
6
4
|
readonly PREFERRED_MAINTENANCE_WINDOW: "Sat:12:30-Sat:20:30";
|
|
7
5
|
/** 1 minute — balances monitoring granularity against CloudWatch costs */
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { Duration } from "aws-cdk-lib";
|
|
2
2
|
export const RDS_DEFAULTS = Object.freeze({
|
|
3
|
-
/** Default database port: FJALL on phone keypad */
|
|
4
|
-
DEFAULT_PORT: 35255,
|
|
5
3
|
/** Saturday window in UTC — minimises weekday impact for scheduled maintenance */
|
|
6
4
|
PREFERRED_MAINTENANCE_WINDOW: "Sat:12:30-Sat:20:30",
|
|
7
5
|
/** 1 minute — balances monitoring granularity against CloudWatch costs */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Duration, RemovalPolicy } from "aws-cdk-lib";
|
|
1
|
+
import { Annotations, Duration, RemovalPolicy } from "aws-cdk-lib";
|
|
2
|
+
import { ENGINE_DEFAULT_DATABASE_PORTS } from "@fjall/generator";
|
|
2
3
|
import { Connections, InstanceType, Port, SubnetType } from "aws-cdk-lib/aws-ec2";
|
|
3
4
|
import { CaCertificate, Credentials, DatabaseInstance, DatabaseInstanceFromSnapshot, DatabaseInstanceEngine, DatabaseInstanceReadReplica, DatabaseProxy, ParameterGroup, PostgresEngineVersion, ProxyTarget, SnapshotCredentials, StorageType } from "aws-cdk-lib/aws-rds";
|
|
4
5
|
import { Runtime } from "aws-cdk-lib/aws-lambda";
|
|
@@ -19,6 +20,7 @@ export class RdsInstance extends Construct {
|
|
|
19
20
|
databaseSecurityGroup;
|
|
20
21
|
vpc;
|
|
21
22
|
port;
|
|
23
|
+
clientPort;
|
|
22
24
|
engineConfig;
|
|
23
25
|
databaseCredentials;
|
|
24
26
|
database;
|
|
@@ -31,11 +33,17 @@ export class RdsInstance extends Construct {
|
|
|
31
33
|
constructor(scope, id, props) {
|
|
32
34
|
super(scope, id);
|
|
33
35
|
this.constructId = id;
|
|
34
|
-
this.port = props.port ?? RDS_DEFAULTS.DEFAULT_PORT;
|
|
35
36
|
this.vpc = props.vpc;
|
|
36
37
|
this.databaseNameValue = props.databaseName ?? id.replace("Rds", "");
|
|
37
38
|
// PostgreSQL fallback for direct usage - ensure engine and engineConfig match
|
|
38
39
|
this.engineConfig = props.engineConfig ?? DEFAULT_POSTGRES_ENGINE_CONFIG;
|
|
40
|
+
const engineDefaultPort = ENGINE_DEFAULT_DATABASE_PORTS[this.engineConfig.family];
|
|
41
|
+
this.port = props.port ?? engineDefaultPort;
|
|
42
|
+
// RDS Proxy listens exclusively on the engine-default port (CreateDBProxy
|
|
43
|
+
// has no port parameter), so the client-facing surface must advertise it
|
|
44
|
+
// whenever a proxy fronts the database.
|
|
45
|
+
const proxyEnabled = props.proxy !== undefined && props.proxy !== false;
|
|
46
|
+
this.clientPort = proxyEnabled ? engineDefaultPort : this.port;
|
|
39
47
|
this.addDatabase(props);
|
|
40
48
|
// Secret rotation enabled by default (opt-out with secretRotation: false)
|
|
41
49
|
const secretRotationDisabled = props.credentials?.secretRotation === false;
|
|
@@ -196,6 +204,19 @@ export class RdsInstance extends Construct {
|
|
|
196
204
|
maxConnectionsPercent: proxyConfig.maxConnections,
|
|
197
205
|
maxIdleConnectionsPercent: proxyConfig.maxIdleConnections
|
|
198
206
|
});
|
|
207
|
+
// The proxy is now the client-facing surface: grantConnect /
|
|
208
|
+
// allowDefaultPortFrom must open the PROXY security group on the proxy's
|
|
209
|
+
// fixed engine-default listener port, not the database SG on the DB port.
|
|
210
|
+
this.connections = new Connections({
|
|
211
|
+
securityGroups: [this.databaseProxySecurityGroup],
|
|
212
|
+
defaultPort: Port.tcp(this.clientPort)
|
|
213
|
+
});
|
|
214
|
+
if (this.port !== this.clientPort) {
|
|
215
|
+
Annotations.of(this).addWarning(`Database '${this.databaseNameValue}' listens on port ${this.port}, but its ` +
|
|
216
|
+
`RDS Proxy accepts client connections only on ${this.clientPort} (the engine ` +
|
|
217
|
+
`default — AWS does not allow configuring the proxy listener port). Fjall ` +
|
|
218
|
+
`advertises ${this.clientPort}; direct-to-database access uses ${this.port}.`);
|
|
219
|
+
}
|
|
199
220
|
addProxyCfnOutput(this, this.constructId, this.databaseNameValue, this.databaseProxy);
|
|
200
221
|
}
|
|
201
222
|
addReadReplica(props) {
|
|
@@ -312,7 +333,7 @@ exports.handler = async (event) => {
|
|
|
312
333
|
}
|
|
313
334
|
}
|
|
314
335
|
getHostPort() {
|
|
315
|
-
return String(this.
|
|
336
|
+
return String(this.clientPort);
|
|
316
337
|
}
|
|
317
338
|
getCredentials() {
|
|
318
339
|
return this.databaseCredentials;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
2
|
import { type KeyValue } from "../../../../types.js";
|
|
3
|
+
import type { CustomerManagedPolicyRef, PermissionsBoundarySpec } from "../../../../config/aws/identityCentreConfig.js";
|
|
3
4
|
export declare class PermissionSet extends Construct {
|
|
4
5
|
private cfnPermissionSet;
|
|
5
6
|
constructor(scope: Construct, id: string, props: {
|
|
@@ -7,6 +8,9 @@ export declare class PermissionSet extends Construct {
|
|
|
7
8
|
instanceArn: string;
|
|
8
9
|
description?: string;
|
|
9
10
|
managedPolicies?: string[];
|
|
11
|
+
inlinePolicy?: Record<string, unknown>;
|
|
12
|
+
customerManagedPolicies?: CustomerManagedPolicyRef[];
|
|
13
|
+
permissionsBoundary?: PermissionsBoundarySpec;
|
|
10
14
|
sessionDuration?: string;
|
|
11
15
|
tags?: KeyValue[];
|
|
12
16
|
});
|
|
@@ -9,6 +9,18 @@ export class PermissionSet extends Construct {
|
|
|
9
9
|
instanceArn: props.instanceArn,
|
|
10
10
|
description: props.description,
|
|
11
11
|
managedPolicies: props.managedPolicies,
|
|
12
|
+
inlinePolicy: props.inlinePolicy,
|
|
13
|
+
customerManagedPolicyReferences: props.customerManagedPolicies?.map((ref) => ({ name: ref.name, path: ref.path })),
|
|
14
|
+
permissionsBoundary: props.permissionsBoundary !== undefined
|
|
15
|
+
? "managedPolicyArn" in props.permissionsBoundary
|
|
16
|
+
? { managedPolicyArn: props.permissionsBoundary.managedPolicyArn }
|
|
17
|
+
: {
|
|
18
|
+
customerManagedPolicyReference: {
|
|
19
|
+
name: props.permissionsBoundary.customerManagedPolicy.name,
|
|
20
|
+
path: props.permissionsBoundary.customerManagedPolicy.path
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
: undefined,
|
|
12
24
|
sessionDuration: props.sessionDuration,
|
|
13
25
|
tags: props.tags?.map((t) => ({ key: t.key, value: t.value }))
|
|
14
26
|
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Construct } from "constructs";
|
|
2
|
+
/** Identity-store user ARNs carry no region or account segment. */
|
|
3
|
+
export declare function identityStoreUserArnPattern(partition: string): string;
|
|
4
|
+
export interface IdentityStoreUserProviderProps {
|
|
5
|
+
identityStoreId: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Shared Provider-framework backend for Custom::FjallIdentityStoreUser —
|
|
9
|
+
* one handler + provider pair serves every declared user in the stack. A
|
|
10
|
+
* plain AwsCustomResource cannot express the create-or-adopt branching the
|
|
11
|
+
* handler implements (aiDocs/designs/2026-07-04-org-user-management-design.md § 2).
|
|
12
|
+
*/
|
|
13
|
+
export declare class IdentityStoreUserProvider extends Construct {
|
|
14
|
+
readonly serviceToken: string;
|
|
15
|
+
constructor(scope: Construct, id: string, props: IdentityStoreUserProviderProps);
|
|
16
|
+
}
|
|
17
|
+
export interface IdentityStoreUserProps {
|
|
18
|
+
serviceToken: string;
|
|
19
|
+
identityStoreId: string;
|
|
20
|
+
email: string;
|
|
21
|
+
givenName: string;
|
|
22
|
+
familyName: string;
|
|
23
|
+
displayName?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A declared identity-store user (fjall-managed / external-manual sources).
|
|
27
|
+
* Pre-existing users are adopted, never overwritten-by-recreate, and adopted
|
|
28
|
+
* users are never deleted on stack removal. `userId` is the CR's UserId
|
|
29
|
+
* attribute — wiring memberships to it gives them an implicit dependency on
|
|
30
|
+
* the user, so CloudFormation deletes memberships first.
|
|
31
|
+
*/
|
|
32
|
+
export declare class IdentityStoreUser extends Construct {
|
|
33
|
+
readonly userId: string;
|
|
34
|
+
constructor(scope: Construct, id: string, props: IdentityStoreUserProps);
|
|
35
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { CustomResource, Stack } from "aws-cdk-lib";
|
|
4
|
+
import { PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
5
|
+
import { Code, Runtime } from "aws-cdk-lib/aws-lambda";
|
|
6
|
+
import { Provider } from "aws-cdk-lib/custom-resources";
|
|
7
|
+
import { Construct } from "constructs";
|
|
8
|
+
import { SingletonFunction } from "../../compute/lambda.js";
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
/**
|
|
11
|
+
* Resolves relative to the compiled location too — the build step copies the
|
|
12
|
+
* asset directory into dist/lib/lambda-assets/identity-store-user/asset/,
|
|
13
|
+
* mirroring the source tree, so the same relative walk works post-build.
|
|
14
|
+
*/
|
|
15
|
+
const LAMBDA_ASSET_DIR = join(__dirname, "../../../../lambda-assets/identity-store-user/asset");
|
|
16
|
+
const HANDLER_TIMEOUT_SECONDS = 120;
|
|
17
|
+
/** Identity-store user ARNs carry no region or account segment. */
|
|
18
|
+
export function identityStoreUserArnPattern(partition) {
|
|
19
|
+
return `arn:${partition}:identitystore:::user/*`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Shared Provider-framework backend for Custom::FjallIdentityStoreUser —
|
|
23
|
+
* one handler + provider pair serves every declared user in the stack. A
|
|
24
|
+
* plain AwsCustomResource cannot express the create-or-adopt branching the
|
|
25
|
+
* handler implements (aiDocs/designs/2026-07-04-org-user-management-design.md § 2).
|
|
26
|
+
*/
|
|
27
|
+
export class IdentityStoreUserProvider extends Construct {
|
|
28
|
+
serviceToken;
|
|
29
|
+
constructor(scope, id, props) {
|
|
30
|
+
super(scope, id);
|
|
31
|
+
const stack = Stack.of(this);
|
|
32
|
+
const identityStoreArn = `arn:${stack.partition}:identitystore::${stack.account}:identitystore/${props.identityStoreId}`;
|
|
33
|
+
const userArnPattern = identityStoreUserArnPattern(stack.partition);
|
|
34
|
+
const handler = new SingletonFunction(this, "Handler", {
|
|
35
|
+
runtime: Runtime.NODEJS_22_X,
|
|
36
|
+
code: Code.fromAsset(LAMBDA_ASSET_DIR),
|
|
37
|
+
handler: "index.handler",
|
|
38
|
+
timeout: HANDLER_TIMEOUT_SECONDS,
|
|
39
|
+
lambdaDescription: "Fjall Identity Centre user custom-resource handler",
|
|
40
|
+
roleDescription: "Execution role for the Fjall identity-store user handler (scoped identitystore user CRUD)",
|
|
41
|
+
inlinePolicy: [
|
|
42
|
+
new PolicyStatement({
|
|
43
|
+
actions: [
|
|
44
|
+
"identitystore:CreateUser",
|
|
45
|
+
"identitystore:UpdateUser",
|
|
46
|
+
"identitystore:DeleteUser",
|
|
47
|
+
"identitystore:GetUserId"
|
|
48
|
+
],
|
|
49
|
+
resources: [identityStoreArn, userArnPattern]
|
|
50
|
+
})
|
|
51
|
+
]
|
|
52
|
+
});
|
|
53
|
+
const provider = new Provider(this, "Provider", {
|
|
54
|
+
onEventHandler: handler
|
|
55
|
+
});
|
|
56
|
+
this.serviceToken = provider.serviceToken;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A declared identity-store user (fjall-managed / external-manual sources).
|
|
61
|
+
* Pre-existing users are adopted, never overwritten-by-recreate, and adopted
|
|
62
|
+
* users are never deleted on stack removal. `userId` is the CR's UserId
|
|
63
|
+
* attribute — wiring memberships to it gives them an implicit dependency on
|
|
64
|
+
* the user, so CloudFormation deletes memberships first.
|
|
65
|
+
*/
|
|
66
|
+
export class IdentityStoreUser extends Construct {
|
|
67
|
+
userId;
|
|
68
|
+
constructor(scope, id, props) {
|
|
69
|
+
super(scope, id);
|
|
70
|
+
const resource = new CustomResource(this, "User", {
|
|
71
|
+
serviceToken: props.serviceToken,
|
|
72
|
+
resourceType: "Custom::FjallIdentityStoreUser",
|
|
73
|
+
properties: {
|
|
74
|
+
IdentityStoreId: props.identityStoreId,
|
|
75
|
+
Email: props.email,
|
|
76
|
+
GivenName: props.givenName,
|
|
77
|
+
FamilyName: props.familyName,
|
|
78
|
+
DisplayName: props.displayName !== undefined && props.displayName.trim() !== ""
|
|
79
|
+
? props.displayName
|
|
80
|
+
: `${props.givenName} ${props.familyName}`
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
this.userId = resource.getAttString("UserId");
|
|
84
|
+
}
|
|
85
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.25.0",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"clean": "rm -rf ./dist",
|
|
35
35
|
"clean:node": "rm -rf ./node_modules",
|
|
36
36
|
"build:cert-gen-lambda": "node lib/lambda-assets/cert-generator/src/build.mjs",
|
|
37
|
-
"build": "npm run build:cert-gen-lambda && tsc && cp -r lib/layers dist/lib/layers && mkdir -p dist/lib/lambda-assets/cert-generator/asset && cp lib/lambda-assets/cert-generator/asset/index.js dist/lib/lambda-assets/cert-generator/asset/index.js && cp lib/lambda-assets/cert-generator/asset/package.json dist/lib/lambda-assets/cert-generator/asset/package.json && cp lib/resources/aws/compute/lifecycleHookLambda.source.cjs dist/lib/resources/aws/compute/lifecycleHookLambda.source.cjs && cp lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs dist/lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs && cp lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs dist/lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs",
|
|
37
|
+
"build": "npm run build:cert-gen-lambda && tsc && cp -r lib/layers dist/lib/layers && mkdir -p dist/lib/lambda-assets/cert-generator/asset && cp lib/lambda-assets/cert-generator/asset/index.js dist/lib/lambda-assets/cert-generator/asset/index.js && cp lib/lambda-assets/cert-generator/asset/package.json dist/lib/lambda-assets/cert-generator/asset/package.json && mkdir -p dist/lib/lambda-assets/identity-store-user/asset && cp lib/lambda-assets/identity-store-user/asset/index.js dist/lib/lambda-assets/identity-store-user/asset/index.js && cp lib/lambda-assets/identity-store-user/asset/package.json dist/lib/lambda-assets/identity-store-user/asset/package.json && cp lib/resources/aws/compute/lifecycleHookLambda.source.cjs dist/lib/resources/aws/compute/lifecycleHookLambda.source.cjs && cp lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs dist/lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs && cp lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs dist/lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs",
|
|
38
38
|
"watch": "tsc -w",
|
|
39
39
|
"watch:only": "tsc -w",
|
|
40
40
|
"test": "vitest run",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@aws-sdk/client-elastic-load-balancing-v2": "^3.1045.0",
|
|
53
|
+
"@aws-sdk/client-identitystore": "^3.1038.0",
|
|
53
54
|
"@peculiar/x509": "2.0.0",
|
|
54
55
|
"@types/aws-lambda": "^8.10.161",
|
|
55
56
|
"@types/node": "^26.0.0",
|
|
@@ -63,8 +64,8 @@
|
|
|
63
64
|
},
|
|
64
65
|
"dependencies": {
|
|
65
66
|
"@aws-sdk/client-organizations": "^3.1038.0",
|
|
66
|
-
"@fjall/generator": "^2.
|
|
67
|
-
"@fjall/util": "^2.
|
|
67
|
+
"@fjall/generator": "^2.25.0",
|
|
68
|
+
"@fjall/util": "^2.25.0",
|
|
68
69
|
"constructs": "^10.6.0"
|
|
69
70
|
},
|
|
70
71
|
"overrides": {
|
|
@@ -78,5 +79,5 @@
|
|
|
78
79
|
"engines": {
|
|
79
80
|
"node": ">=18.0.0"
|
|
80
81
|
},
|
|
81
|
-
"gitHead": "
|
|
82
|
+
"gitHead": "7c1a329184064aefa557c2c09de0965c4f8cd4fb"
|
|
82
83
|
}
|