@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,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 {
|
|
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 {
|
|
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.
|
|
38
|
-
this.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
51
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
...(
|
|
140
|
-
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
|
-
|
|
143
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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;
|