@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,28 +1,40 @@
1
1
  import { type Construct } from "constructs";
2
2
  import { NestedStack, type NestedStackProps } from "aws-cdk-lib";
3
3
  import { type KeyValue } from "../../types.js";
4
+ import { type IdentityCentreConfig } from "./identityCentreConfig.js";
4
5
  interface IdentityCenterProps extends NestedStackProps {
6
+ /** Workload accounts (management excluded by the Organisation construct). */
5
7
  accounts: KeyValue;
8
+ /** Required only when a permission set opts into the management account. */
9
+ managementAccountId?: string;
10
+ config?: IdentityCentreConfig;
6
11
  tags?: KeyValue[];
7
12
  }
8
- export interface PermissionSetConfig {
9
- managedPolicies: string[];
10
- description?: string;
11
- sessionDuration?: string;
12
- }
13
- export type CustomPermissionSets = Record<string, PermissionSetConfig>;
14
13
  export declare class IdentityCenter extends NestedStack {
15
14
  identityStoreId: string;
16
15
  identityCenterArn: string;
17
16
  private listInstancesRole;
18
17
  private accountsConfig;
19
18
  private customTags?;
19
+ private readonly config?;
20
+ private readonly managementAccountId?;
21
+ private groupConstructs;
20
22
  private groupIds;
23
+ private declaredUsers;
24
+ private lookupUserIds;
21
25
  constructor(scope: Construct, id: string, props: IdentityCenterProps);
22
- declarePermissionSets(customs: CustomPermissionSets): void;
23
- assignGroupMembers(members: Record<string, string[]>): void;
24
26
  private createListInstancesRole;
25
27
  private listIdentityCenterInstance;
28
+ private createGroup;
29
+ private createDeclaredUsers;
30
+ /** Maps the shared name derivation to account ids. Only the appended
31
+ * "management" pseudo-account lacks an ACCOUNTS entry. */
32
+ private resolveTargetAccounts;
26
33
  private createPermissionSetAndAssignments;
34
+ private createMemberships;
35
+ /** One lookup CR per email, shared across groups. `resolutionVersion` is
36
+ * baked into the physical id so bumping it re-runs every resolution —
37
+ * the UUID-rotation heal (`fjall user rebind`). */
38
+ private lookupUserId;
27
39
  }
28
40
  export {};
@@ -1,83 +1,60 @@
1
+ import { createHash } from "node:crypto";
1
2
  import * as customResources from "aws-cdk-lib/custom-resources";
2
3
  import { PolicyDocument, PolicyStatement, ServicePrincipal } from "aws-cdk-lib/aws-iam";
3
- import { CfnOutput, NestedStack } from "aws-cdk-lib";
4
+ import { Annotations, CfnOutput, NestedStack } from "aws-cdk-lib";
4
5
  import { AwsCustomResource } from "../../resources/aws/utilities/awsCustomResource.js";
5
- import { stripAndCamelCase } from "../../utils/stripAndCamelCase.js";
6
- import { Group, PermissionSet, Assignment } from "../../resources/aws/iam/identityCenter/index.js";
6
+ import { Group, GroupMembership, PermissionSet, Assignment, IdentityStoreUser, IdentityStoreUserProvider, identityStoreUserArnPattern } from "../../resources/aws/iam/identityCenter/index.js";
7
7
  import { ManagedPolicy, Role } from "../../resources/aws/iam/index.js";
8
- import { IdentityCenterMembership } from "./identityCenterMembership.js";
9
- const defaultPermissionSets = {
10
- AdministratorAccess: {
11
- Policy: "arn:aws:iam::aws:policy/AdministratorAccess",
12
- Description: "Permission set for associated AdministratorAccess policy"
13
- },
14
- ReadOnlyAccess: {
15
- Policy: "arn:aws:iam::aws:policy/ReadOnlyAccess",
16
- Description: "Permission set for associated ReadOnlyAccess policy"
17
- },
18
- Billing: {
19
- Policy: "arn:aws:iam::aws:policy/AWSBillingReadOnlyAccess",
20
- Description: "Permission set for associated Billing policy"
21
- }
22
- };
8
+ import { allGroupNames, assignmentConstructId, boundGroupsForSet, effectivePermissionSets, implicitGroupNames, resolveTargetAccountNames, validateIdentityCentreConfig } from "./identityCentreConfig.js";
23
9
  // CreateAccountAssignment has a 15-outstanding-async limit. Batching at 8
24
10
  // keeps headroom for CFN's internal retries within the same backoff window.
25
11
  const ASSIGNMENT_BATCH_SIZE = 8;
12
+ /** 8-char content hash — construct-id suffix safe for any email (full address,
13
+ * so same-local-part addresses at different domains never collide). */
14
+ function emailHash(email) {
15
+ return createHash("sha256").update(email).digest("hex").slice(0, 8);
16
+ }
26
17
  export class IdentityCenter extends NestedStack {
27
18
  identityStoreId;
28
19
  identityCenterArn;
29
20
  listInstancesRole;
30
21
  accountsConfig;
31
22
  customTags;
23
+ config;
24
+ managementAccountId;
25
+ groupConstructs = {};
32
26
  groupIds = {};
27
+ declaredUsers = {};
28
+ lookupUserIds = {};
33
29
  constructor(scope, id, props) {
34
30
  super(scope, id, props);
35
31
  this.accountsConfig = props.accounts;
36
32
  this.customTags = props.tags;
37
- this.createListInstancesRole();
38
- this.listIdentityCenterInstance();
39
- for (const [name, config] of Object.entries(defaultPermissionSets)) {
40
- this.createPermissionSetAndAssignments(name, config, {
41
- managedPolicies: [config.Policy]
33
+ this.config = props.config;
34
+ this.managementAccountId = props.managementAccountId;
35
+ if (props.config !== undefined) {
36
+ const { warnings, infos } = validateIdentityCentreConfig(props.config, {
37
+ workloadAccountNames: Object.keys(props.accounts)
42
38
  });
43
- }
44
- }
45
- declarePermissionSets(customs) {
46
- for (const [name, config] of Object.entries(customs)) {
47
- if (this.groupIds[name] !== undefined) {
48
- throw new Error(`Permission set "${name}" already declared (collision with default or prior declaration)`);
39
+ for (const warning of warnings) {
40
+ Annotations.of(this).addWarningV2(`fjall:identity-centre:${warning.code}`, warning.message);
49
41
  }
50
- const primaryPolicy = config.managedPolicies[0];
51
- if (primaryPolicy === undefined) {
52
- throw new Error(`Permission set "${name}" must declare at least one managed policy.`);
42
+ for (const info of infos) {
43
+ Annotations.of(this).addInfo(info.message);
53
44
  }
54
- this.createPermissionSetAndAssignments(name, {
55
- Policy: primaryPolicy,
56
- ...(config.description !== undefined && {
57
- Description: config.description
58
- })
59
- }, {
60
- managedPolicies: config.managedPolicies,
61
- ...(config.sessionDuration !== undefined && {
62
- sessionDuration: config.sessionDuration
63
- })
64
- });
65
45
  }
66
- }
67
- assignGroupMembers(members) {
68
- for (const [groupName, emails] of Object.entries(members)) {
69
- if (emails.length === 0)
70
- continue;
71
- const groupId = this.groupIds[groupName];
72
- if (groupId === undefined) {
73
- throw new Error(`Cannot assign members to undeclared group "${groupName}". Declare via declarePermissionSets() first, or use one of the defaults: AdministratorAccess, ReadOnlyAccess, Billing.`);
74
- }
75
- new IdentityCenterMembership(this, `${groupName}Members`, {
76
- identityStoreId: this.identityStoreId,
77
- groupId,
78
- groupMembers: emails
79
- });
46
+ this.createListInstancesRole();
47
+ this.listIdentityCenterInstance();
48
+ const sets = effectivePermissionSets(this.config);
49
+ const implicitGroups = new Set(implicitGroupNames(sets));
50
+ for (const name of allGroupNames(this.config)) {
51
+ this.createGroup(name, implicitGroups.has(name));
52
+ }
53
+ this.createDeclaredUsers();
54
+ for (const [name, spec] of Object.entries(sets)) {
55
+ this.createPermissionSetAndAssignments(name, spec);
80
56
  }
57
+ this.createMemberships();
81
58
  }
82
59
  createListInstancesRole() {
83
60
  this.listInstancesRole = new Role(this, "IdentityCenterCustomResourceRole", {
@@ -126,49 +103,181 @@ export class IdentityCenter extends NestedStack {
126
103
  exportName: "identityStoreId"
127
104
  });
128
105
  }
129
- createPermissionSetAndAssignments(name, config, permissionSetOverrides) {
106
+ createGroup(name, isImplicitSetGroup) {
107
+ const description = this.config?.groups?.[name]?.description ??
108
+ (isImplicitSetGroup
109
+ ? `Group for associated ${name} permission set`
110
+ : "Fjall-managed Identity Centre group");
130
111
  const group = new Group(this, `${name}Group`, {
131
112
  displayName: name,
132
113
  identityStoreId: this.identityStoreId,
133
- description: `Group for associated ${name} permission set`
114
+ description
134
115
  });
116
+ this.groupConstructs[name] = group;
135
117
  this.groupIds[name] = group.getGroupId();
118
+ new CfnOutput(this, `${name}GroupId`, {
119
+ key: `${name}GroupId`,
120
+ value: group.getGroupId(),
121
+ exportName: `${name}GroupId`
122
+ });
123
+ }
124
+ createDeclaredUsers() {
125
+ const users = this.config !== undefined && "users" in this.config
126
+ ? (this.config.users ?? {})
127
+ : {};
128
+ if (Object.keys(users).length === 0) {
129
+ return;
130
+ }
131
+ const provider = new IdentityStoreUserProvider(this, "UserProvider", {
132
+ identityStoreId: this.identityStoreId
133
+ });
134
+ for (const [email, spec] of Object.entries(users)) {
135
+ this.declaredUsers[email] = new IdentityStoreUser(this, `User${emailHash(email)}`, {
136
+ serviceToken: provider.serviceToken,
137
+ identityStoreId: this.identityStoreId,
138
+ email,
139
+ givenName: spec.givenName,
140
+ familyName: spec.familyName,
141
+ ...(spec.displayName !== undefined && {
142
+ displayName: spec.displayName
143
+ })
144
+ });
145
+ }
146
+ }
147
+ /** Maps the shared name derivation to account ids. Only the appended
148
+ * "management" pseudo-account lacks an ACCOUNTS entry. */
149
+ resolveTargetAccounts(name, spec) {
150
+ const targets = [];
151
+ for (const accountName of resolveTargetAccountNames(spec, Object.keys(this.accountsConfig))) {
152
+ const accountId = this.accountsConfig[accountName];
153
+ if (accountId !== undefined) {
154
+ targets.push([accountName, accountId]);
155
+ continue;
156
+ }
157
+ if (this.managementAccountId === undefined) {
158
+ throw new Error(`permissionSets["${name}"] sets includeManagementAccount but no managementAccountId was provided to IdentityCenter`);
159
+ }
160
+ targets.push([accountName, this.managementAccountId]);
161
+ }
162
+ return targets;
163
+ }
164
+ createPermissionSetAndAssignments(name, spec) {
165
+ const boundGroups = boundGroupsForSet(name, spec);
136
166
  const permissionSet = new PermissionSet(this, `PermissionSet${name}`, {
137
167
  name: name,
138
168
  instanceArn: this.identityCenterArn,
139
- ...(config.Description !== undefined && {
140
- description: config.Description
169
+ ...(spec.description !== undefined && {
170
+ description: spec.description
171
+ }),
172
+ ...(spec.managedPolicies !== undefined &&
173
+ spec.managedPolicies.length > 0 && {
174
+ managedPolicies: spec.managedPolicies
175
+ }),
176
+ ...(spec.sessionDuration !== undefined && {
177
+ sessionDuration: spec.sessionDuration
178
+ }),
179
+ ...(spec.inlinePolicy !== undefined && {
180
+ inlinePolicy: spec.inlinePolicy
181
+ }),
182
+ ...(spec.customerManagedPolicies !== undefined &&
183
+ spec.customerManagedPolicies.length > 0 && {
184
+ customerManagedPolicies: spec.customerManagedPolicies
141
185
  }),
142
- managedPolicies: permissionSetOverrides.managedPolicies,
143
- ...(permissionSetOverrides.sessionDuration !== undefined && {
144
- sessionDuration: permissionSetOverrides.sessionDuration
186
+ ...(spec.permissionsBoundary !== undefined && {
187
+ permissionsBoundary: spec.permissionsBoundary
145
188
  }),
146
189
  ...(this.customTags !== undefined && { tags: this.customTags })
147
190
  });
148
- permissionSet.node.addDependency(group);
149
- new CfnOutput(this, `${name}GroupId`, {
150
- key: `${name}GroupId`,
151
- value: group.getGroupId(),
152
- exportName: `${name}GroupId`
153
- });
191
+ for (const groupName of boundGroups) {
192
+ const group = this.groupConstructs[groupName];
193
+ if (group !== undefined) {
194
+ permissionSet.node.addDependency(group);
195
+ }
196
+ }
154
197
  const assignments = [];
155
- for (const [accountName, accountId] of Object.entries(this.accountsConfig)) {
156
- const assignment = new Assignment(this, `${stripAndCamelCase(accountName)}${name}Assignment`, {
157
- instanceArn: this.identityCenterArn,
158
- permissionSetArn: permissionSet.getPermissionSetArn(),
159
- principalType: "GROUP",
160
- principalId: group.getGroupId(),
161
- targetType: "AWS_ACCOUNT",
162
- targetId: accountId
163
- });
164
- assignment.node.addDependency(permissionSet);
165
- if (assignments.length >= ASSIGNMENT_BATCH_SIZE) {
166
- const dependency = assignments[assignments.length - ASSIGNMENT_BATCH_SIZE];
167
- if (dependency !== undefined) {
168
- assignment.node.addDependency(dependency);
198
+ for (const [accountName, accountId] of this.resolveTargetAccounts(name, spec)) {
199
+ for (const groupName of boundGroups) {
200
+ const idSuffix = assignmentConstructId(accountName, name, groupName);
201
+ const groupId = this.groupIds[groupName];
202
+ if (groupId === undefined) {
203
+ throw new Error(`Internal: group "${groupName}" missing for permission set "${name}" — validation should have caught this`);
169
204
  }
205
+ const assignment = new Assignment(this, idSuffix, {
206
+ instanceArn: this.identityCenterArn,
207
+ permissionSetArn: permissionSet.getPermissionSetArn(),
208
+ principalType: "GROUP",
209
+ principalId: groupId,
210
+ targetType: "AWS_ACCOUNT",
211
+ targetId: accountId
212
+ });
213
+ assignment.node.addDependency(permissionSet);
214
+ if (assignments.length >= ASSIGNMENT_BATCH_SIZE) {
215
+ const dependency = assignments[assignments.length - ASSIGNMENT_BATCH_SIZE];
216
+ if (dependency !== undefined) {
217
+ assignment.node.addDependency(dependency);
218
+ }
219
+ }
220
+ assignments.push(assignment);
221
+ }
222
+ }
223
+ }
224
+ createMemberships() {
225
+ for (const [groupName, emails] of Object.entries(this.config?.memberships ?? {})) {
226
+ const groupId = this.groupIds[groupName];
227
+ if (groupId === undefined || emails.length === 0) {
228
+ continue;
170
229
  }
171
- assignments.push(assignment);
230
+ for (const email of emails) {
231
+ const declared = this.declaredUsers[email];
232
+ const userId = declared !== undefined ? declared.userId : this.lookupUserId(email);
233
+ new GroupMembership(this, `${groupName}Membership${emailHash(email)}`, {
234
+ identityStoreId: this.identityStoreId,
235
+ groupId,
236
+ userId
237
+ });
238
+ }
239
+ }
240
+ }
241
+ /** One lookup CR per email, shared across groups. `resolutionVersion` is
242
+ * baked into the physical id so bumping it re-runs every resolution —
243
+ * the UUID-rotation heal (`fjall user rebind`). */
244
+ lookupUserId(email) {
245
+ const existing = this.lookupUserIds[email];
246
+ if (existing !== undefined) {
247
+ return existing;
172
248
  }
249
+ const resolutionVersion = this.config?.resolutionVersion ?? 1;
250
+ const getUserIdCall = {
251
+ service: "@aws-sdk/client-identitystore",
252
+ action: "GetUserId",
253
+ parameters: {
254
+ IdentityStoreId: this.identityStoreId,
255
+ AlternateIdentifier: {
256
+ UniqueAttribute: {
257
+ AttributePath: "emails.value",
258
+ AttributeValue: email
259
+ }
260
+ }
261
+ },
262
+ physicalResourceId: customResources.PhysicalResourceId.of(`fjallGetUserId:${email}:v${resolutionVersion}`)
263
+ };
264
+ const identityStoreArn = `arn:${this.partition}:identitystore::${this.account}:identitystore/${this.identityStoreId}`;
265
+ const lookup = new AwsCustomResource(this, `UserLookup${emailHash(email)}`, {
266
+ onCreate: getUserIdCall,
267
+ onUpdate: getUserIdCall,
268
+ resourceType: "Custom::FjallIdentityStoreUserLookup",
269
+ policy: customResources.AwsCustomResourcePolicy.fromStatements([
270
+ new PolicyStatement({
271
+ actions: ["identitystore:GetUserId"],
272
+ resources: [
273
+ identityStoreArn,
274
+ identityStoreUserArnPattern(this.partition)
275
+ ]
276
+ })
277
+ ])
278
+ });
279
+ const userId = lookup.getResponseField("UserId");
280
+ this.lookupUserIds[email] = userId;
281
+ return userId;
173
282
  }
174
283
  }
@@ -0,0 +1,147 @@
1
+ import { type DefaultPermissionSetName } from "@fjall/generator";
2
+ /**
3
+ * Customer-facing `identityCentre` configuration for the Organisation
4
+ * construct. The discriminated union on `source` mirrors AWS ground truth:
5
+ * the identity source of an Identity Center instance is neither readable nor
6
+ * settable via any supported API, so the mode is user-declared and
7
+ * authoritative. Design: aiDocs/designs/2026-07-04-org-user-management-design.md
8
+ *
9
+ * The source vocabulary is canonical in @fjall/generator
10
+ * (organisationSchemas.ts) — re-exported here so existing consumers keep
11
+ * their import path.
12
+ */
13
+ export { IDENTITY_CENTRE_SOURCES, type IdentityCentreSource } from "@fjall/generator";
14
+ export interface IdentityCentreUserSpec {
15
+ givenName: string;
16
+ familyName: string;
17
+ /** Defaults to `${givenName} ${familyName}`. */
18
+ displayName?: string;
19
+ /**
20
+ * ISO date (e.g. "2026-09-30"). A past-expiry user still holding any
21
+ * membership fails synth, naming the user and the groups to detach.
22
+ */
23
+ expires?: string;
24
+ }
25
+ export interface CustomerManagedPolicyRef {
26
+ name: string;
27
+ path?: string;
28
+ }
29
+ export type PermissionsBoundarySpec = {
30
+ managedPolicyArn: string;
31
+ } | {
32
+ customerManagedPolicy: CustomerManagedPolicyRef;
33
+ };
34
+ export interface PermissionSetSpec {
35
+ managedPolicies?: string[];
36
+ /** Raw IAM policy document (escape hatch — passed through to CFN verbatim). */
37
+ inlinePolicy?: Record<string, unknown>;
38
+ customerManagedPolicies?: CustomerManagedPolicyRef[];
39
+ permissionsBoundary?: PermissionsBoundarySpec;
40
+ /** ISO-8601 duration, e.g. "PT12H". */
41
+ sessionDuration?: string;
42
+ description?: string;
43
+ /**
44
+ * ACCOUNTS-map names this set is assigned to. Default "all" (every
45
+ * workload account). The management account is reachable only via
46
+ * `includeManagementAccount`.
47
+ */
48
+ accounts?: string[] | "all";
49
+ /** Management-account opt-in — the only route to it. Default false. */
50
+ includeManagementAccount?: boolean;
51
+ /**
52
+ * Groups bound to this set (many-to-many). Omitted or empty = an implicit
53
+ * same-named group is created and bound.
54
+ */
55
+ groups?: string[];
56
+ }
57
+ export interface IdentityCentreGroupSpec {
58
+ description?: string;
59
+ }
60
+ interface IdentityCentreBase {
61
+ /** Custom groups beyond the per-set implicit ones. */
62
+ groups?: Record<string, IdentityCentreGroupSpec>;
63
+ /**
64
+ * Custom permission sets. An entry named after a built-in default merges
65
+ * over that default's spec (tune sessionDuration/accounts/groups of a
66
+ * built-in without restating its policy).
67
+ */
68
+ permissionSets?: Record<string, PermissionSetSpec>;
69
+ /** Group name -> member emails. */
70
+ memberships?: Record<string, string[]>;
71
+ /**
72
+ * Bump to force every external email lookup to re-resolve — heals
73
+ * memberships after an IdP deletes and recreates a user (UUID rotation).
74
+ * Default 1.
75
+ */
76
+ resolutionVersion?: number;
77
+ /** false suppresses the three built-in permission sets. Default true. */
78
+ defaultPermissionSets?: boolean;
79
+ }
80
+ export type IdentityCentreConfig = (IdentityCentreBase & {
81
+ source: "fjall-managed";
82
+ users: Record<string, IdentityCentreUserSpec>;
83
+ }) | (IdentityCentreBase & {
84
+ source: "external";
85
+ users?: never;
86
+ }) | (IdentityCentreBase & {
87
+ source: "external-manual";
88
+ users?: Record<string, IdentityCentreUserSpec>;
89
+ });
90
+ export interface DefaultPermissionSetDefinition {
91
+ managedPolicyArn: string;
92
+ description: string;
93
+ }
94
+ export declare const DEFAULT_PERMISSION_SETS: Record<DefaultPermissionSetName, DefaultPermissionSetDefinition>;
95
+ export interface ResolvedPermissionSet extends PermissionSetSpec {
96
+ isDefault: boolean;
97
+ }
98
+ /**
99
+ * Defaults (unless suppressed) with same-named config entries merged over
100
+ * them, plus the remaining custom sets. Consumed by both the validator and
101
+ * the IdentityCenter construct — the two MUST agree on this derivation.
102
+ */
103
+ export declare function effectivePermissionSets(config?: IdentityCentreConfig): Record<string, ResolvedPermissionSet>;
104
+ /**
105
+ * Canonical names of the accounts a permission set is assigned to, in
106
+ * declaration order: explicit `accounts` entries resolve case-insensitively
107
+ * to the workload ACCOUNTS names (unknown entries drop out — Rule 4 reports
108
+ * them), otherwise every workload account; the management opt-in appends the
109
+ * "management" pseudo-account. Duplicate entries are preserved so the
110
+ * validator's duplicate-declaration detection still fires. Consumed by both
111
+ * the validator (Rule 8 collision detection) and the IdentityCenter
112
+ * construct — the two MUST agree on this derivation.
113
+ */
114
+ export declare function resolveTargetAccountNames(spec: PermissionSetSpec, workloadAccountNames: readonly string[]): string[];
115
+ /** Groups bound to a set: explicit list, else the implicit same-named group. */
116
+ export declare function boundGroupsForSet(name: string, spec: PermissionSetSpec): string[];
117
+ export declare function implicitGroupNames(sets: Record<string, ResolvedPermissionSet>): string[];
118
+ /**
119
+ * Assignment construct-id derivation shared by the validator (collision
120
+ * detection) and the IdentityCenter construct — the two MUST agree. The
121
+ * implicit same-named group omits the group segment to keep the legacy id
122
+ * shape, so existing deployments do not replace their assignments.
123
+ */
124
+ export declare function assignmentConstructId(accountName: string, setName: string, groupName: string): string;
125
+ /** Every group the config will create: implicit per-set groups + custom groups. */
126
+ export declare function allGroupNames(config?: IdentityCentreConfig): string[];
127
+ export interface IdentityCentreValidationIssue {
128
+ code: string;
129
+ message: string;
130
+ }
131
+ export interface IdentityCentreValidationResult {
132
+ warnings: IdentityCentreValidationIssue[];
133
+ infos: IdentityCentreValidationIssue[];
134
+ }
135
+ export interface IdentityCentreValidationContext {
136
+ /** Workload account names from the ACCOUNTS map (management excluded). */
137
+ workloadAccountNames: string[];
138
+ /** Injectable clock for expiry checks; defaults to the wall clock. */
139
+ now?: Date;
140
+ }
141
+ /**
142
+ * Synth-time validation — every rule fires before any CloudFormation call so
143
+ * a typo never surfaces as a mid-deploy custom-resource failure. Collects
144
+ * every violation and throws once; warnings/infos are returned for the
145
+ * construct to surface via CDK Annotations.
146
+ */
147
+ export declare function validateIdentityCentreConfig(config: IdentityCentreConfig, context: IdentityCentreValidationContext): IdentityCentreValidationResult;