@fjall/components-infrastructure 2.23.0 → 2.24.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/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,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.24.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.24.0",
|
|
68
|
+
"@fjall/util": "^2.24.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": "616d7af846f0eebfa3409f5f47c98fbee836bad9"
|
|
82
83
|
}
|