@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.
@@ -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 CustomPermissionSets } from "../../config/aws/identityCenter.js";
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
- identityCenter?: boolean;
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 setupIdentityCenter;
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.identityCenter ?? true, managementAccountId);
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(identityCenter, managementAccountId) {
137
- this.setupIdentityCenter(identityCenter, managementAccountId);
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
- setupIdentityCenter(identityCenter, managementAccountId) {
161
- if (identityCenter) {
162
- // Exclude the management account from SSO assignments — only root
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
- this.identityCenter.declarePermissionSets(customs);
180
- }
181
- assignGroupMembers(members) {
182
- if (this.identityCenter === undefined) {
183
- throw new Error("Identity Center is not enabled. Pass identityCenter: true to OrganisationFactory.");
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.assignGroupMembers(members);
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 suffix becomes `<service>-<target>-latest`.
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;
@@ -2,3 +2,4 @@ export * from "./assignment.js";
2
2
  export * from "./group.js";
3
3
  export * from "./groupMembership.js";
4
4
  export * from "./permissionSet.js";
5
+ export * from "./user.js";
@@ -2,3 +2,4 @@ export * from "./assignment.js";
2
2
  export * from "./group.js";
3
3
  export * from "./groupMembership.js";
4
4
  export * from "./permissionSet.js";
5
+ export * from "./user.js";
@@ -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.23.0",
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.23.0",
67
- "@fjall/util": "^2.23.0",
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": "dadd84e6118f03252877974eec755e812cb5b42f"
82
+ "gitHead": "616d7af846f0eebfa3409f5f47c98fbee836bad9"
82
83
  }