@gugananuvem/aws-local-simulator 1.0.34 → 1.0.39
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/README.md +662 -658
- package/package.json +11 -4
- package/src/config/default-config.js +71 -0
- package/src/config/env-loader.js +4 -2
- package/src/middleware/authorization.js +271 -0
- package/src/middleware/cloudtrail-logger.js +333 -0
- package/src/middleware/error-handler.js +138 -0
- package/src/middleware/index.js +79 -0
- package/src/middleware/policy-enforcement.js +407 -0
- package/src/server.js +74 -4
- package/src/services/acm/index.js +66 -0
- package/src/services/acm/models/certificate.js +202 -0
- package/src/services/acm/server.js +224 -0
- package/src/services/acm/simulator.js +293 -0
- package/src/services/alb/index.js +104 -0
- package/src/services/alb/models/loadBalancer.js +88 -0
- package/src/services/alb/models/targetGroup.js +128 -0
- package/src/services/alb/server.js +160 -0
- package/src/services/alb/simulator.js +262 -0
- package/src/services/apigateway/index.js +22 -0
- package/src/services/apigateway/server.js +247 -35
- package/src/services/apigateway/simulator.js +780 -26
- package/src/services/asg/index.js +100 -0
- package/src/services/asg/models/autoScalingGroup.js +110 -0
- package/src/services/asg/models/launchTemplate.js +41 -0
- package/src/services/asg/models/scalingPolicy.js +51 -0
- package/src/services/asg/server.js +236 -0
- package/src/services/asg/simulator.js +410 -0
- package/src/services/aurora/cluster-manager.js +731 -0
- package/src/services/aurora/data-api.js +514 -0
- package/src/services/aurora/db-engine.js +270 -0
- package/src/services/aurora/index.js +159 -0
- package/src/services/aurora/rds-api.js +189 -0
- package/src/services/aurora/server.js +213 -0
- package/src/services/aurora/sql-translator.js +334 -0
- package/src/services/cloudformation/index.js +2 -2
- package/src/services/cloudformation/server.js +1 -0
- package/src/services/cloudfront/cache-engine.js +225 -0
- package/src/services/cloudfront/geo-restrictions.js +377 -0
- package/src/services/cloudfront/index.js +220 -0
- package/src/services/cloudfront/models/behavior.js +158 -0
- package/src/services/cloudfront/models/distribution.js +284 -0
- package/src/services/cloudfront/models/index.js +99 -0
- package/src/services/cloudfront/models/invalidation.js +109 -0
- package/src/services/cloudfront/models/origin.js +155 -0
- package/src/services/cloudfront/origin-router.js +552 -0
- package/src/services/cloudfront/origin-validator.js +398 -0
- package/src/services/cloudfront/server.js +512 -0
- package/src/services/cloudfront/signed-url-generator.js +377 -0
- package/src/services/cloudfront/simulator.js +425 -0
- package/src/services/cloudfront/store.js +326 -0
- package/src/services/cloudshell/env-builder.js +27 -0
- package/src/services/cloudshell/index.js +48 -0
- package/src/services/cloudshell/routes.js +60 -0
- package/src/services/cloudshell/session-manager.js +226 -0
- package/src/services/cloudshell/static/.gitkeep +0 -0
- package/src/services/cloudshell/static/index.html +406 -0
- package/src/services/cloudshell/websocket-handler.js +107 -0
- package/src/services/cloudtrail/index.js +2 -3
- package/src/services/cloudtrail/server.js +68 -0
- package/src/services/cognito/server.js +4 -1
- package/src/services/dynamodb/server.js +4 -1
- package/src/services/dynamodb/simulator.js +2 -1
- package/src/services/ec2/index.js +101 -0
- package/src/services/ec2/instanceServer.js +311 -0
- package/src/services/ec2/models/ami.js +39 -0
- package/src/services/ec2/models/instance.js +98 -0
- package/src/services/ec2/models/networkInterface.js +74 -0
- package/src/services/ec2/models/securityGroup.js +71 -0
- package/src/services/ec2/server.js +350 -0
- package/src/services/ec2/simulator.js +436 -0
- package/src/services/eventbridge/server.js +36 -0
- package/src/services/eventbridge/simulator.js +230 -45
- package/src/services/iam/index.js +65 -0
- package/src/services/iam/middleware.js +99 -0
- package/src/services/iam/server.js +331 -0
- package/src/services/iam/simulator.js +1432 -0
- package/src/services/index.js +28 -0
- package/src/services/kinesis-analytics/index.js +105 -0
- package/src/services/kinesis-analytics/models/application.js +325 -0
- package/src/services/kinesis-analytics/models/index.js +12 -0
- package/src/services/kinesis-analytics/server.js +250 -0
- package/src/services/kinesis-analytics/simulator.js +407 -0
- package/src/services/kinesis-analytics/store.js +176 -0
- package/src/services/kinesis-data-streams/index.js +106 -0
- package/src/services/kinesis-data-streams/models/index.js +12 -0
- package/src/services/kinesis-data-streams/models/record.js +70 -0
- package/src/services/kinesis-data-streams/models/stream.js +280 -0
- package/src/services/kinesis-data-streams/server.js +255 -0
- package/src/services/kinesis-data-streams/simulator.js +514 -0
- package/src/services/kinesis-data-streams/store.js +163 -0
- package/src/services/kinesis-firehose/index.js +106 -0
- package/src/services/kinesis-firehose/models/delivery-stream.js +265 -0
- package/src/services/kinesis-firehose/models/index.js +10 -0
- package/src/services/kinesis-firehose/server.js +215 -0
- package/src/services/kinesis-firehose/simulator.js +388 -0
- package/src/services/kinesis-firehose/store.js +163 -0
- package/src/services/kms/server.js +4 -1
- package/src/services/lambda/handler-loader.js +50 -5
- package/src/services/lambda/index.js +26 -0
- package/src/services/lambda/server.js +18 -12
- package/src/services/lambda/simulator.js +73 -17
- package/src/services/opensearch/index.js +105 -0
- package/src/services/opensearch/models/document.js +115 -0
- package/src/services/opensearch/models/index.js +73 -0
- package/src/services/opensearch/server.js +310 -0
- package/src/services/opensearch/simulator.js +605 -0
- package/src/services/opensearch/store.js +162 -0
- package/src/services/organizations/control-tower-simulator.js +321 -0
- package/src/services/organizations/control-tower-store.js +182 -0
- package/src/services/organizations/iam-integration.js +413 -0
- package/src/services/organizations/index.js +172 -0
- package/src/services/organizations/models/index.js +13 -0
- package/src/services/organizations/models/invitation.js +48 -0
- package/src/services/organizations/models/organization.js +566 -0
- package/src/services/organizations/models/role.js +244 -0
- package/src/services/organizations/models/scp.js +131 -0
- package/src/services/organizations/server.js +736 -0
- package/src/services/organizations/simulator.js +593 -0
- package/src/services/organizations/store.js +235 -0
- package/src/services/organizations/vpc-integration.js +453 -0
- package/src/services/route53/index.js +67 -0
- package/src/services/route53/models/healthCheck.js +101 -0
- package/src/services/route53/models/hostedZone.js +37 -0
- package/src/services/route53/models/resourceRecord.js +106 -0
- package/src/services/route53/server.js +841 -0
- package/src/services/route53/simulator.js +350 -0
- package/src/services/s3/index.js +62 -0
- package/src/services/s3/presigned-url-generator.js +337 -0
- package/src/services/s3/server.js +231 -0
- package/src/services/s3/simulator.js +551 -4
- package/src/services/sns/index.js +31 -0
- package/src/services/sns/server.js +9 -0
- package/src/services/sqs/index.js +38 -0
- package/src/services/sqs/server.js +57 -4
- package/src/services/sqs/simulator.js +423 -33
- package/src/services/sts/simulator.js +209 -17
- package/src/services/vpc/README.md +459 -0
- package/src/services/vpc/ec2ResponseFormatter.js +431 -0
- package/src/services/vpc/index.js +90 -0
- package/src/services/vpc/models/internetGateway.js +71 -0
- package/src/services/vpc/models/routeTable.js +160 -0
- package/src/services/vpc/models/subnet.js +48 -0
- package/src/services/vpc/models/vpc.js +37 -0
- package/src/services/vpc/models/vpcEndpoint.js +100 -0
- package/src/services/vpc/models/vpcGateway.js +134 -0
- package/src/services/vpc/server.js +836 -0
- package/src/services/vpc/simulator.js +613 -0
- package/src/utils/policy-cache.js +215 -0
- package/aws-config +0 -154
|
@@ -0,0 +1,1432 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const logger = require('../../utils/logger');
|
|
5
|
+
|
|
6
|
+
const DATA_DIR = path.join(process.cwd(), '.aws-local-simulator-data', 'iam');
|
|
7
|
+
|
|
8
|
+
const FILE_MAP = {
|
|
9
|
+
users: 'users.json',
|
|
10
|
+
groups: 'groups.json',
|
|
11
|
+
roles: 'roles.json',
|
|
12
|
+
policies: 'policies.json',
|
|
13
|
+
accessKeys: 'access-keys.json',
|
|
14
|
+
userPolicies: 'user-policies.json',
|
|
15
|
+
groupPolicies: 'group-policies.json',
|
|
16
|
+
rolePolicies: 'role-policies.json',
|
|
17
|
+
groupMembers: 'group-members.json',
|
|
18
|
+
userGroups: 'user-groups.json'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
class IAMSimulator {
|
|
22
|
+
constructor(config) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.users = new Map();
|
|
25
|
+
this.groups = new Map();
|
|
26
|
+
this.roles = new Map();
|
|
27
|
+
this.policies = new Map();
|
|
28
|
+
this.accessKeys = new Map();
|
|
29
|
+
this.userPolicies = new Map();
|
|
30
|
+
this.groupPolicies = new Map();
|
|
31
|
+
this.rolePolicies = new Map();
|
|
32
|
+
this.groupMembers = new Map();
|
|
33
|
+
this.userGroups = new Map();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async initialize() {
|
|
37
|
+
logger.debug('Inicializando IAM Simulator...');
|
|
38
|
+
await this._load();
|
|
39
|
+
await this._provisionFromConfig();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- Access Key Management ---
|
|
43
|
+
|
|
44
|
+
async createAccessKey(params) {
|
|
45
|
+
const { UserName } = params;
|
|
46
|
+
|
|
47
|
+
if (!this.users.has(UserName)) {
|
|
48
|
+
const err = new Error(`The user with name ${UserName} cannot be found.`);
|
|
49
|
+
err.code = 'NoSuchEntity';
|
|
50
|
+
err.statusCode = 404;
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const AccessKeyId = 'AKIA' + crypto.randomBytes(8).toString('hex').toUpperCase();
|
|
55
|
+
const SecretAccessKey = crypto.randomBytes(20).toString('hex');
|
|
56
|
+
|
|
57
|
+
const record = {
|
|
58
|
+
AccessKeyId,
|
|
59
|
+
SecretAccessKey,
|
|
60
|
+
UserName,
|
|
61
|
+
Status: 'Active',
|
|
62
|
+
CreateDate: new Date().toISOString()
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
this.accessKeys.set(AccessKeyId, record);
|
|
66
|
+
await this._persist();
|
|
67
|
+
|
|
68
|
+
return { AccessKey: record };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async listAccessKeys(params) {
|
|
72
|
+
const { UserName } = params;
|
|
73
|
+
|
|
74
|
+
if (!this.users.has(UserName)) {
|
|
75
|
+
const err = new Error(`The user with name ${UserName} cannot be found.`);
|
|
76
|
+
err.code = 'NoSuchEntity';
|
|
77
|
+
err.statusCode = 404;
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const filteredKeys = [];
|
|
82
|
+
for (const record of this.accessKeys.values()) {
|
|
83
|
+
if (record.UserName === UserName) {
|
|
84
|
+
filteredKeys.push({
|
|
85
|
+
AccessKeyId: record.AccessKeyId,
|
|
86
|
+
UserName: record.UserName,
|
|
87
|
+
Status: record.Status,
|
|
88
|
+
CreateDate: record.CreateDate
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { AccessKeyMetadata: filteredKeys };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async deleteAccessKey(params) {
|
|
97
|
+
const { AccessKeyId } = params;
|
|
98
|
+
|
|
99
|
+
if (!this.accessKeys.has(AccessKeyId)) {
|
|
100
|
+
const err = new Error(`The Access Key with id ${AccessKeyId} cannot be found.`);
|
|
101
|
+
err.code = 'NoSuchEntity';
|
|
102
|
+
err.statusCode = 404;
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.accessKeys.delete(AccessKeyId);
|
|
107
|
+
await this._persist();
|
|
108
|
+
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async updateAccessKey(params) {
|
|
113
|
+
const { AccessKeyId, Status, UserName } = params || {};
|
|
114
|
+
|
|
115
|
+
if (!this.accessKeys.has(AccessKeyId)) {
|
|
116
|
+
const err = new Error(`The Access Key with id ${AccessKeyId} cannot be found.`);
|
|
117
|
+
err.code = 'NoSuchEntity';
|
|
118
|
+
err.statusCode = 404;
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const record = this.accessKeys.get(AccessKeyId);
|
|
123
|
+
if (UserName && record.UserName !== UserName) {
|
|
124
|
+
const err = new Error(`The access key with id ${AccessKeyId} does not belong to user ${UserName}.`);
|
|
125
|
+
err.code = 'NoSuchEntity';
|
|
126
|
+
err.statusCode = 404;
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (Status) {
|
|
131
|
+
record.Status = Status;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await this._persist();
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- Group Management ---
|
|
139
|
+
|
|
140
|
+
async createGroup(params) {
|
|
141
|
+
const { GroupName, Path = '/' } = params || {};
|
|
142
|
+
|
|
143
|
+
if (!GroupName) {
|
|
144
|
+
const err = new Error('1 validation error detected: Value at \'groupName\' failed to satisfy constraint: Member must not be null');
|
|
145
|
+
err.code = 'ValidationError';
|
|
146
|
+
err.statusCode = 400;
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (this.groups.has(GroupName)) {
|
|
151
|
+
const err = new Error(`Group with name ${GroupName} already exists.`);
|
|
152
|
+
err.code = 'EntityAlreadyExists';
|
|
153
|
+
err.statusCode = 409;
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const GroupId = 'AGPA' + crypto.randomUUID().replace(/-/g, '').substring(0, 21).toUpperCase();
|
|
158
|
+
const Arn = `arn:aws:iam::123456789012:group${Path}${GroupName}`;
|
|
159
|
+
const CreateDate = new Date().toISOString();
|
|
160
|
+
|
|
161
|
+
const group = { GroupName, GroupId, Arn, Path, CreateDate };
|
|
162
|
+
|
|
163
|
+
this.groups.set(GroupName, group);
|
|
164
|
+
this.groupPolicies.set(GroupName, new Set());
|
|
165
|
+
this.groupMembers.set(GroupName, new Set());
|
|
166
|
+
|
|
167
|
+
await this._persist();
|
|
168
|
+
|
|
169
|
+
return { Group: group };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async getGroup(params) {
|
|
173
|
+
const { GroupName } = params || {};
|
|
174
|
+
|
|
175
|
+
if (!this.groups.has(GroupName)) {
|
|
176
|
+
const err = new Error(`The group with name ${GroupName} cannot be found.`);
|
|
177
|
+
err.code = 'NoSuchEntity';
|
|
178
|
+
err.statusCode = 404;
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const groupRecord = this.groups.get(GroupName);
|
|
183
|
+
const members = this.groupMembers.get(GroupName) || new Set();
|
|
184
|
+
const Users = [];
|
|
185
|
+
|
|
186
|
+
for (const userName of members) {
|
|
187
|
+
const userRecord = this.users.get(userName);
|
|
188
|
+
if (userRecord) {
|
|
189
|
+
Users.push(userRecord);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { Group: groupRecord, Users };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async deleteGroup(params) {
|
|
197
|
+
const { GroupName } = params || {};
|
|
198
|
+
|
|
199
|
+
if (!this.groups.has(GroupName)) {
|
|
200
|
+
const err = new Error(`The group with name ${GroupName} cannot be found.`);
|
|
201
|
+
err.code = 'NoSuchEntity';
|
|
202
|
+
err.statusCode = 404;
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const members = this.groupMembers.get(GroupName);
|
|
207
|
+
if (members && members.size > 0) {
|
|
208
|
+
const err = new Error(`Cannot delete group ${GroupName} - it still contains members.`);
|
|
209
|
+
err.code = 'DeleteConflict';
|
|
210
|
+
err.statusCode = 409;
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Decrement attachment count for all policies attached to this group
|
|
215
|
+
const groupPolicies = this.groupPolicies.get(GroupName);
|
|
216
|
+
if (groupPolicies) {
|
|
217
|
+
for (const policyArn of groupPolicies) {
|
|
218
|
+
const policyRecord = this.policies.get(policyArn);
|
|
219
|
+
if (policyRecord) {
|
|
220
|
+
policyRecord.AttachmentCount = Math.max(0, policyRecord.AttachmentCount - 1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.groups.delete(GroupName);
|
|
226
|
+
this.groupPolicies.delete(GroupName);
|
|
227
|
+
this.groupMembers.delete(GroupName);
|
|
228
|
+
|
|
229
|
+
await this._persist();
|
|
230
|
+
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async addUserToGroup(params) {
|
|
235
|
+
const { GroupName, UserName } = params || {};
|
|
236
|
+
|
|
237
|
+
if (!this.groups.has(GroupName)) {
|
|
238
|
+
const err = new Error(`The group with name ${GroupName} cannot be found.`);
|
|
239
|
+
err.code = 'NoSuchEntity';
|
|
240
|
+
err.statusCode = 404;
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!this.users.has(UserName)) {
|
|
245
|
+
const err = new Error(`The user with name ${UserName} cannot be found.`);
|
|
246
|
+
err.code = 'NoSuchEntity';
|
|
247
|
+
err.statusCode = 404;
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.groupMembers.get(GroupName).add(UserName);
|
|
252
|
+
this.userGroups.get(UserName).add(GroupName);
|
|
253
|
+
|
|
254
|
+
await this._persist();
|
|
255
|
+
|
|
256
|
+
return {};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async removeUserFromGroup(params) {
|
|
260
|
+
const { GroupName, UserName } = params || {};
|
|
261
|
+
|
|
262
|
+
if (!this.groups.has(GroupName)) {
|
|
263
|
+
const err = new Error(`The group with name ${GroupName} cannot be found.`);
|
|
264
|
+
err.code = 'NoSuchEntity';
|
|
265
|
+
err.statusCode = 404;
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!this.users.has(UserName)) {
|
|
270
|
+
const err = new Error(`The user with name ${UserName} cannot be found.`);
|
|
271
|
+
err.code = 'NoSuchEntity';
|
|
272
|
+
err.statusCode = 404;
|
|
273
|
+
throw err;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
this.groupMembers.get(GroupName).delete(UserName);
|
|
277
|
+
this.userGroups.get(UserName).delete(GroupName);
|
|
278
|
+
|
|
279
|
+
await this._persist();
|
|
280
|
+
|
|
281
|
+
return {};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async listGroupsForUser(params) {
|
|
285
|
+
const { UserName } = params || {};
|
|
286
|
+
|
|
287
|
+
if (!this.users.has(UserName)) {
|
|
288
|
+
const err = new Error(`The user with name ${UserName} cannot be found.`);
|
|
289
|
+
err.code = 'NoSuchEntity';
|
|
290
|
+
err.statusCode = 404;
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const groupNames = this.userGroups.get(UserName) || new Set();
|
|
295
|
+
const Groups = [];
|
|
296
|
+
|
|
297
|
+
for (const groupName of groupNames) {
|
|
298
|
+
const groupRecord = this.groups.get(groupName);
|
|
299
|
+
if (groupRecord) {
|
|
300
|
+
Groups.push(groupRecord);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return { Groups };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async listGroups(params) {
|
|
308
|
+
const { PathPrefix = '/' } = params || {};
|
|
309
|
+
|
|
310
|
+
const filteredGroups = [];
|
|
311
|
+
for (const group of this.groups.values()) {
|
|
312
|
+
if (group.Path.startsWith(PathPrefix)) {
|
|
313
|
+
filteredGroups.push(group);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { Groups: filteredGroups };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// --- Role Management ---
|
|
321
|
+
|
|
322
|
+
async createRole(params) {
|
|
323
|
+
const { RoleName, Path = '/', AssumeRolePolicyDocument } = params || {};
|
|
324
|
+
|
|
325
|
+
if (!RoleName) {
|
|
326
|
+
const err = new Error('1 validation error detected: Value at \'roleName\' failed to satisfy constraint: Member must not be null');
|
|
327
|
+
err.code = 'ValidationError';
|
|
328
|
+
err.statusCode = 400;
|
|
329
|
+
throw err;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (this.roles.has(RoleName)) {
|
|
333
|
+
const err = new Error(`Role with name ${RoleName} already exists.`);
|
|
334
|
+
err.code = 'EntityAlreadyExists';
|
|
335
|
+
err.statusCode = 409;
|
|
336
|
+
throw err;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const RoleId = 'AROA' + crypto.randomUUID().replace(/-/g, '').substring(0, 21).toUpperCase();
|
|
340
|
+
const Arn = `arn:aws:iam::123456789012:role${Path}${RoleName}`;
|
|
341
|
+
const CreateDate = new Date().toISOString();
|
|
342
|
+
|
|
343
|
+
let parsedAssumeRolePolicyDocument = AssumeRolePolicyDocument;
|
|
344
|
+
if (typeof AssumeRolePolicyDocument === 'string') {
|
|
345
|
+
try {
|
|
346
|
+
parsedAssumeRolePolicyDocument = JSON.parse(AssumeRolePolicyDocument);
|
|
347
|
+
} catch (e) {
|
|
348
|
+
// Keep as string if parsing fails
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const role = {
|
|
353
|
+
RoleName,
|
|
354
|
+
RoleId,
|
|
355
|
+
Arn,
|
|
356
|
+
Path,
|
|
357
|
+
AssumeRolePolicyDocument: parsedAssumeRolePolicyDocument,
|
|
358
|
+
CreateDate
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
this.roles.set(RoleName, role);
|
|
362
|
+
this.rolePolicies.set(RoleName, new Set());
|
|
363
|
+
|
|
364
|
+
await this._persist();
|
|
365
|
+
|
|
366
|
+
return { Role: role };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async getRole(params) {
|
|
370
|
+
const { RoleName } = params || {};
|
|
371
|
+
|
|
372
|
+
if (!this.roles.has(RoleName)) {
|
|
373
|
+
const err = new Error(`The role with name ${RoleName} cannot be found.`);
|
|
374
|
+
err.code = 'NoSuchEntity';
|
|
375
|
+
err.statusCode = 404;
|
|
376
|
+
throw err;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const roleRecord = this.roles.get(RoleName);
|
|
380
|
+
const attachedPolicies = this.rolePolicies.get(RoleName) || new Set();
|
|
381
|
+
|
|
382
|
+
return { Role: roleRecord, AttachedPolicies: [...attachedPolicies] };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async listRoles(params) {
|
|
386
|
+
const { PathPrefix = '/' } = params || {};
|
|
387
|
+
|
|
388
|
+
const filteredRoles = [];
|
|
389
|
+
for (const role of this.roles.values()) {
|
|
390
|
+
if (role.Path.startsWith(PathPrefix)) {
|
|
391
|
+
filteredRoles.push(role);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return { Roles: filteredRoles };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async deleteRole(params) {
|
|
399
|
+
const { RoleName } = params || {};
|
|
400
|
+
|
|
401
|
+
if (!this.roles.has(RoleName)) {
|
|
402
|
+
const err = new Error(`The role with name ${RoleName} cannot be found.`);
|
|
403
|
+
err.code = 'NoSuchEntity';
|
|
404
|
+
err.statusCode = 404;
|
|
405
|
+
throw err;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const attachedPolicies = this.rolePolicies.get(RoleName);
|
|
409
|
+
if (attachedPolicies && attachedPolicies.size > 0) {
|
|
410
|
+
const err = new Error(`Cannot delete role ${RoleName} - it has attached policies.`);
|
|
411
|
+
err.code = 'DeleteConflict';
|
|
412
|
+
err.statusCode = 409;
|
|
413
|
+
throw err;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
this.roles.delete(RoleName);
|
|
417
|
+
this.rolePolicies.delete(RoleName);
|
|
418
|
+
|
|
419
|
+
await this._persist();
|
|
420
|
+
|
|
421
|
+
return {};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// --- Policy Management ---
|
|
425
|
+
|
|
426
|
+
_validatePolicyDocument(document) {
|
|
427
|
+
let parsed = document;
|
|
428
|
+
|
|
429
|
+
if (typeof document === 'string') {
|
|
430
|
+
try {
|
|
431
|
+
parsed = JSON.parse(document);
|
|
432
|
+
} catch (e) {
|
|
433
|
+
const err = new Error('Policy document is not valid JSON.');
|
|
434
|
+
err.code = 'MalformedPolicyDocument';
|
|
435
|
+
err.statusCode = 400;
|
|
436
|
+
throw err;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (!parsed || !Array.isArray(parsed.Statement) || parsed.Statement.length === 0) {
|
|
441
|
+
const err = new Error('Policy document must contain a Statement array with at least one element.');
|
|
442
|
+
err.code = 'MalformedPolicyDocument';
|
|
443
|
+
err.statusCode = 400;
|
|
444
|
+
throw err;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
for (const statement of parsed.Statement) {
|
|
448
|
+
if (!statement.Effect || (statement.Effect !== 'Allow' && statement.Effect !== 'Deny')) {
|
|
449
|
+
const err = new Error('Policy statement must have an Effect field with value "Allow" or "Deny".');
|
|
450
|
+
err.code = 'MalformedPolicyDocument';
|
|
451
|
+
err.statusCode = 400;
|
|
452
|
+
throw err;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (!statement.Action) {
|
|
456
|
+
const err = new Error('Policy statement must have an Action field.');
|
|
457
|
+
err.code = 'MalformedPolicyDocument';
|
|
458
|
+
err.statusCode = 400;
|
|
459
|
+
throw err;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!statement.Resource) {
|
|
463
|
+
const err = new Error('Policy statement must have a Resource field.');
|
|
464
|
+
err.code = 'MalformedPolicyDocument';
|
|
465
|
+
err.statusCode = 400;
|
|
466
|
+
throw err;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return parsed;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async createPolicy(params) {
|
|
474
|
+
const { PolicyName, Path = '/', PolicyDocument } = params || {};
|
|
475
|
+
|
|
476
|
+
if (!PolicyName) {
|
|
477
|
+
const err = new Error('1 validation error detected: Value at \'policyName\' failed to satisfy constraint: Member must not be null');
|
|
478
|
+
err.code = 'ValidationError';
|
|
479
|
+
err.statusCode = 400;
|
|
480
|
+
throw err;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const document = this._validatePolicyDocument(PolicyDocument);
|
|
484
|
+
|
|
485
|
+
const Arn = `arn:aws:iam::123456789012:policy${Path}${PolicyName}`;
|
|
486
|
+
|
|
487
|
+
if (this.policies.has(Arn)) {
|
|
488
|
+
const err = new Error(`A policy called ${PolicyName} already exists. Duplicate names are not allowed.`);
|
|
489
|
+
err.code = 'EntityAlreadyExists';
|
|
490
|
+
err.statusCode = 409;
|
|
491
|
+
throw err;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const PolicyId = 'ANPA' + crypto.randomUUID().replace(/-/g, '').substring(0, 21).toUpperCase();
|
|
495
|
+
const CreateDate = new Date().toISOString();
|
|
496
|
+
|
|
497
|
+
const policy = {
|
|
498
|
+
PolicyName,
|
|
499
|
+
PolicyId,
|
|
500
|
+
Arn,
|
|
501
|
+
Path,
|
|
502
|
+
DefaultVersionId: 'v1',
|
|
503
|
+
Document: document,
|
|
504
|
+
CreateDate,
|
|
505
|
+
AttachmentCount: 0
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
this.policies.set(Arn, policy);
|
|
509
|
+
await this._persist();
|
|
510
|
+
|
|
511
|
+
// Return metadata only (without Document) wrapped in Policy
|
|
512
|
+
const { Document, ...metadata } = policy;
|
|
513
|
+
return { Policy: metadata };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async getPolicy(params) {
|
|
517
|
+
const { PolicyArn } = params || {};
|
|
518
|
+
|
|
519
|
+
if (!this.policies.has(PolicyArn)) {
|
|
520
|
+
const err = new Error(`Policy ${PolicyArn} does not exist or is not attachable.`);
|
|
521
|
+
err.code = 'NoSuchEntity';
|
|
522
|
+
err.statusCode = 404;
|
|
523
|
+
throw err;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const policyRecord = this.policies.get(PolicyArn);
|
|
527
|
+
const { Document, ...metadata } = policyRecord;
|
|
528
|
+
return { Policy: metadata };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async getPolicyVersion(params) {
|
|
532
|
+
const { PolicyArn, VersionId } = params || {};
|
|
533
|
+
|
|
534
|
+
if (!this.policies.has(PolicyArn)) {
|
|
535
|
+
const err = new Error(`Policy ${PolicyArn} does not exist or is not attachable.`);
|
|
536
|
+
err.code = 'NoSuchEntity';
|
|
537
|
+
err.statusCode = 404;
|
|
538
|
+
throw err;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const policyRecord = this.policies.get(PolicyArn);
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
PolicyVersion: {
|
|
545
|
+
Document: policyRecord.Document,
|
|
546
|
+
VersionId: policyRecord.DefaultVersionId,
|
|
547
|
+
IsDefaultVersion: true,
|
|
548
|
+
CreateDate: policyRecord.CreateDate
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async listPolicies(params) {
|
|
554
|
+
const { PathPrefix = '/', Scope } = params || {};
|
|
555
|
+
|
|
556
|
+
const filteredPolicies = [];
|
|
557
|
+
for (const policy of this.policies.values()) {
|
|
558
|
+
if (!policy.Path.startsWith(PathPrefix)) {
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// If Scope === 'Local', only return local policies (all of ours are local)
|
|
563
|
+
if (Scope === 'Local' || Scope === undefined || Scope === null || Scope === 'All') {
|
|
564
|
+
const { Document, ...metadata } = policy;
|
|
565
|
+
filteredPolicies.push(metadata);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return { Policies: filteredPolicies };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async deletePolicy(params) {
|
|
573
|
+
const { PolicyArn } = params || {};
|
|
574
|
+
|
|
575
|
+
if (!this.policies.has(PolicyArn)) {
|
|
576
|
+
const err = new Error(`Policy ${PolicyArn} does not exist or is not attachable.`);
|
|
577
|
+
err.code = 'NoSuchEntity';
|
|
578
|
+
err.statusCode = 404;
|
|
579
|
+
throw err;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const policyRecord = this.policies.get(PolicyArn);
|
|
583
|
+
|
|
584
|
+
if (policyRecord.AttachmentCount > 0) {
|
|
585
|
+
const err = new Error(`Cannot delete policy ${PolicyArn} - it is attached to entities.`);
|
|
586
|
+
err.code = 'DeleteConflict';
|
|
587
|
+
err.statusCode = 409;
|
|
588
|
+
throw err;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
this.policies.delete(PolicyArn);
|
|
592
|
+
await this._persist();
|
|
593
|
+
|
|
594
|
+
return {};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// --- Policy Attach/Detach ---
|
|
598
|
+
|
|
599
|
+
async attachUserPolicy(params) {
|
|
600
|
+
const { UserName, PolicyArn } = params || {};
|
|
601
|
+
|
|
602
|
+
if (!this.users.has(UserName)) {
|
|
603
|
+
const err = new Error(`The user with name ${UserName} cannot be found.`);
|
|
604
|
+
err.code = 'NoSuchEntity';
|
|
605
|
+
err.statusCode = 404;
|
|
606
|
+
throw err;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (!this.policies.has(PolicyArn)) {
|
|
610
|
+
const err = new Error(`Policy ${PolicyArn} does not exist or is not attachable.`);
|
|
611
|
+
err.code = 'NoSuchEntity';
|
|
612
|
+
err.statusCode = 404;
|
|
613
|
+
throw err;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const userPolicySet = this.userPolicies.get(UserName);
|
|
617
|
+
if (!userPolicySet.has(PolicyArn)) {
|
|
618
|
+
userPolicySet.add(PolicyArn);
|
|
619
|
+
const policyRecord = this.policies.get(PolicyArn);
|
|
620
|
+
policyRecord.AttachmentCount++;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
await this._persist();
|
|
624
|
+
|
|
625
|
+
return {};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
async attachGroupPolicy(params) {
|
|
629
|
+
const { GroupName, PolicyArn } = params || {};
|
|
630
|
+
|
|
631
|
+
if (!this.groups.has(GroupName)) {
|
|
632
|
+
const err = new Error(`The group with name ${GroupName} cannot be found.`);
|
|
633
|
+
err.code = 'NoSuchEntity';
|
|
634
|
+
err.statusCode = 404;
|
|
635
|
+
throw err;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (!this.policies.has(PolicyArn)) {
|
|
639
|
+
const err = new Error(`Policy ${PolicyArn} does not exist or is not attachable.`);
|
|
640
|
+
err.code = 'NoSuchEntity';
|
|
641
|
+
err.statusCode = 404;
|
|
642
|
+
throw err;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const groupPolicySet = this.groupPolicies.get(GroupName);
|
|
646
|
+
if (!groupPolicySet.has(PolicyArn)) {
|
|
647
|
+
groupPolicySet.add(PolicyArn);
|
|
648
|
+
const policyRecord = this.policies.get(PolicyArn);
|
|
649
|
+
policyRecord.AttachmentCount++;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
await this._persist();
|
|
653
|
+
|
|
654
|
+
return {};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async attachRolePolicy(params) {
|
|
658
|
+
const { RoleName, PolicyArn } = params || {};
|
|
659
|
+
|
|
660
|
+
if (!this.roles.has(RoleName)) {
|
|
661
|
+
const err = new Error(`The role with name ${RoleName} cannot be found.`);
|
|
662
|
+
err.code = 'NoSuchEntity';
|
|
663
|
+
err.statusCode = 404;
|
|
664
|
+
throw err;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (!this.policies.has(PolicyArn)) {
|
|
668
|
+
const err = new Error(`Policy ${PolicyArn} does not exist or is not attachable.`);
|
|
669
|
+
err.code = 'NoSuchEntity';
|
|
670
|
+
err.statusCode = 404;
|
|
671
|
+
throw err;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const rolePolicySet = this.rolePolicies.get(RoleName);
|
|
675
|
+
if (!rolePolicySet.has(PolicyArn)) {
|
|
676
|
+
rolePolicySet.add(PolicyArn);
|
|
677
|
+
const policyRecord = this.policies.get(PolicyArn);
|
|
678
|
+
policyRecord.AttachmentCount++;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
await this._persist();
|
|
682
|
+
|
|
683
|
+
return {};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async detachUserPolicy(params) {
|
|
687
|
+
const { UserName, PolicyArn } = params || {};
|
|
688
|
+
|
|
689
|
+
if (!this.users.has(UserName)) {
|
|
690
|
+
const err = new Error(`The user with name ${UserName} cannot be found.`);
|
|
691
|
+
err.code = 'NoSuchEntity';
|
|
692
|
+
err.statusCode = 404;
|
|
693
|
+
throw err;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (!this.policies.has(PolicyArn)) {
|
|
697
|
+
const err = new Error(`Policy ${PolicyArn} does not exist or is not attachable.`);
|
|
698
|
+
err.code = 'NoSuchEntity';
|
|
699
|
+
err.statusCode = 404;
|
|
700
|
+
throw err;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const userPolicySet = this.userPolicies.get(UserName);
|
|
704
|
+
if (userPolicySet.has(PolicyArn)) {
|
|
705
|
+
userPolicySet.delete(PolicyArn);
|
|
706
|
+
const policyRecord = this.policies.get(PolicyArn);
|
|
707
|
+
policyRecord.AttachmentCount = Math.max(0, policyRecord.AttachmentCount - 1);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
await this._persist();
|
|
711
|
+
|
|
712
|
+
return {};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async detachGroupPolicy(params) {
|
|
716
|
+
const { GroupName, PolicyArn } = params || {};
|
|
717
|
+
|
|
718
|
+
if (!this.groups.has(GroupName)) {
|
|
719
|
+
const err = new Error(`The group with name ${GroupName} cannot be found.`);
|
|
720
|
+
err.code = 'NoSuchEntity';
|
|
721
|
+
err.statusCode = 404;
|
|
722
|
+
throw err;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (!this.policies.has(PolicyArn)) {
|
|
726
|
+
const err = new Error(`Policy ${PolicyArn} does not exist or is not attachable.`);
|
|
727
|
+
err.code = 'NoSuchEntity';
|
|
728
|
+
err.statusCode = 404;
|
|
729
|
+
throw err;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const groupPolicySet = this.groupPolicies.get(GroupName);
|
|
733
|
+
if (groupPolicySet.has(PolicyArn)) {
|
|
734
|
+
groupPolicySet.delete(PolicyArn);
|
|
735
|
+
const policyRecord = this.policies.get(PolicyArn);
|
|
736
|
+
policyRecord.AttachmentCount = Math.max(0, policyRecord.AttachmentCount - 1);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
await this._persist();
|
|
740
|
+
|
|
741
|
+
return {};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async detachRolePolicy(params) {
|
|
745
|
+
const { RoleName, PolicyArn } = params || {};
|
|
746
|
+
|
|
747
|
+
if (!this.roles.has(RoleName)) {
|
|
748
|
+
const err = new Error(`The role with name ${RoleName} cannot be found.`);
|
|
749
|
+
err.code = 'NoSuchEntity';
|
|
750
|
+
err.statusCode = 404;
|
|
751
|
+
throw err;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (!this.policies.has(PolicyArn)) {
|
|
755
|
+
const err = new Error(`Policy ${PolicyArn} does not exist or is not attachable.`);
|
|
756
|
+
err.code = 'NoSuchEntity';
|
|
757
|
+
err.statusCode = 404;
|
|
758
|
+
throw err;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const rolePolicySet = this.rolePolicies.get(RoleName);
|
|
762
|
+
if (rolePolicySet.has(PolicyArn)) {
|
|
763
|
+
rolePolicySet.delete(PolicyArn);
|
|
764
|
+
const policyRecord = this.policies.get(PolicyArn);
|
|
765
|
+
policyRecord.AttachmentCount = Math.max(0, policyRecord.AttachmentCount - 1);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
await this._persist();
|
|
769
|
+
|
|
770
|
+
return {};
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async listAttachedUserPolicies(params) {
|
|
774
|
+
const { UserName, PathPrefix = '/' } = params || {};
|
|
775
|
+
|
|
776
|
+
if (!this.users.has(UserName)) {
|
|
777
|
+
const err = new Error(`The user with name ${UserName} cannot be found.`);
|
|
778
|
+
err.code = 'NoSuchEntity';
|
|
779
|
+
err.statusCode = 404;
|
|
780
|
+
throw err;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const policyArns = this.userPolicies.get(UserName) || new Set();
|
|
784
|
+
const AttachedPolicies = [];
|
|
785
|
+
|
|
786
|
+
for (const policyArn of policyArns) {
|
|
787
|
+
const policyRecord = this.policies.get(policyArn);
|
|
788
|
+
if (policyRecord && policyRecord.Path.startsWith(PathPrefix)) {
|
|
789
|
+
AttachedPolicies.push({
|
|
790
|
+
PolicyName: policyRecord.PolicyName,
|
|
791
|
+
PolicyArn: policyRecord.Arn
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return { AttachedPolicies };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
async listAttachedGroupPolicies(params) {
|
|
800
|
+
const { GroupName, PathPrefix = '/' } = params || {};
|
|
801
|
+
|
|
802
|
+
if (!this.groups.has(GroupName)) {
|
|
803
|
+
const err = new Error(`The group with name ${GroupName} cannot be found.`);
|
|
804
|
+
err.code = 'NoSuchEntity';
|
|
805
|
+
err.statusCode = 404;
|
|
806
|
+
throw err;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const policyArns = this.groupPolicies.get(GroupName) || new Set();
|
|
810
|
+
const AttachedPolicies = [];
|
|
811
|
+
|
|
812
|
+
for (const policyArn of policyArns) {
|
|
813
|
+
const policyRecord = this.policies.get(policyArn);
|
|
814
|
+
if (policyRecord && policyRecord.Path.startsWith(PathPrefix)) {
|
|
815
|
+
AttachedPolicies.push({
|
|
816
|
+
PolicyName: policyRecord.PolicyName,
|
|
817
|
+
PolicyArn: policyRecord.Arn
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return { AttachedPolicies };
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
async listAttachedRolePolicies(params) {
|
|
826
|
+
const { RoleName, PathPrefix = '/' } = params || {};
|
|
827
|
+
|
|
828
|
+
if (!this.roles.has(RoleName)) {
|
|
829
|
+
const err = new Error(`The role with name ${RoleName} cannot be found.`);
|
|
830
|
+
err.code = 'NoSuchEntity';
|
|
831
|
+
err.statusCode = 404;
|
|
832
|
+
throw err;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const policyArns = this.rolePolicies.get(RoleName) || new Set();
|
|
836
|
+
const AttachedPolicies = [];
|
|
837
|
+
|
|
838
|
+
for (const policyArn of policyArns) {
|
|
839
|
+
const policyRecord = this.policies.get(policyArn);
|
|
840
|
+
if (policyRecord && policyRecord.Path.startsWith(PathPrefix)) {
|
|
841
|
+
AttachedPolicies.push({
|
|
842
|
+
PolicyName: policyRecord.PolicyName,
|
|
843
|
+
PolicyArn: policyRecord.Arn
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return { AttachedPolicies };
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async getAccountSummary(params) {
|
|
852
|
+
const Users = this.users.size;
|
|
853
|
+
const Groups = this.groups.size;
|
|
854
|
+
const Roles = this.roles.size;
|
|
855
|
+
const Policies = this.policies.size;
|
|
856
|
+
const AccessKeys = this.accessKeys.size;
|
|
857
|
+
|
|
858
|
+
let GroupPolicyAttachments = 0;
|
|
859
|
+
for (const policySet of this.groupPolicies.values()) {
|
|
860
|
+
GroupPolicyAttachments += policySet.size;
|
|
861
|
+
}
|
|
862
|
+
let UserPolicyAttachments = 0;
|
|
863
|
+
for (const policySet of this.userPolicies.values()) {
|
|
864
|
+
UserPolicyAttachments += policySet.size;
|
|
865
|
+
}
|
|
866
|
+
let RolePolicyAttachments = 0;
|
|
867
|
+
for (const policySet of this.rolePolicies.values()) {
|
|
868
|
+
RolePolicyAttachments += policySet.size;
|
|
869
|
+
}
|
|
870
|
+
const AttachedPoliciesPerGroupQuota = 10;
|
|
871
|
+
const AttachedPoliciesPerRoleQuota = 10;
|
|
872
|
+
const AttachedPoliciesPerUserQuota = 10;
|
|
873
|
+
const GroupQuota = 300;
|
|
874
|
+
const UserQuota = 5000;
|
|
875
|
+
const RoleQuota = 1000;
|
|
876
|
+
const PolicyQuota = 1500;
|
|
877
|
+
const PolicyVersionsInUseQuota = 10000;
|
|
878
|
+
const ServerCertificatesQuota = 20;
|
|
879
|
+
const SigningCertificatesPerUserQuota = 2;
|
|
880
|
+
const AccessKeysPerUserQuota = 2;
|
|
881
|
+
const MFADevices = 0;
|
|
882
|
+
const MFADevicesInUse = 0;
|
|
883
|
+
const AccountMFAEnabled = 0;
|
|
884
|
+
const SSLCertificates = 0;
|
|
885
|
+
const ServerCertificates = 0;
|
|
886
|
+
|
|
887
|
+
return {
|
|
888
|
+
SummaryMap: {
|
|
889
|
+
Users,
|
|
890
|
+
Groups,
|
|
891
|
+
Roles,
|
|
892
|
+
Policies,
|
|
893
|
+
AccessKeys,
|
|
894
|
+
GroupPolicyAttachments,
|
|
895
|
+
UserPolicyAttachments,
|
|
896
|
+
RolePolicyAttachments,
|
|
897
|
+
AttachedPoliciesPerGroupQuota,
|
|
898
|
+
AttachedPoliciesPerRoleQuota,
|
|
899
|
+
AttachedPoliciesPerUserQuota,
|
|
900
|
+
GroupQuota,
|
|
901
|
+
UserQuota,
|
|
902
|
+
RoleQuota,
|
|
903
|
+
PolicyQuota,
|
|
904
|
+
PolicyVersionsInUseQuota,
|
|
905
|
+
ServerCertificatesQuota,
|
|
906
|
+
SigningCertificatesPerUserQuota,
|
|
907
|
+
AccessKeysPerUserQuota,
|
|
908
|
+
MFADevices,
|
|
909
|
+
MFADevicesInUse,
|
|
910
|
+
AccountMFAEnabled,
|
|
911
|
+
SSLCertificates,
|
|
912
|
+
ServerCertificates
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// --- Policy Evaluation Engine ---
|
|
918
|
+
|
|
919
|
+
resolveIdentity(accessKeyId) {
|
|
920
|
+
const record = this.accessKeys.get(accessKeyId);
|
|
921
|
+
if (!record) return null;
|
|
922
|
+
return { type: 'user', userName: record.UserName, accessKeyId };
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
_collectPolicies(identity) {
|
|
926
|
+
const allStatements = [];
|
|
927
|
+
|
|
928
|
+
// Get user's directly attached policies
|
|
929
|
+
const userPolicyArns = this.userPolicies.get(identity.userName);
|
|
930
|
+
const policyArns = new Set();
|
|
931
|
+
|
|
932
|
+
if (userPolicyArns) {
|
|
933
|
+
for (const arn of userPolicyArns) {
|
|
934
|
+
policyArns.add(arn);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Get all groups the user belongs to and their policies
|
|
939
|
+
const groups = this.userGroups.get(identity.userName);
|
|
940
|
+
if (groups) {
|
|
941
|
+
for (const groupName of groups) {
|
|
942
|
+
const groupPolicyArns = this.groupPolicies.get(groupName);
|
|
943
|
+
if (groupPolicyArns) {
|
|
944
|
+
for (const arn of groupPolicyArns) {
|
|
945
|
+
policyArns.add(arn);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// For each policy ARN, get the full policy Document and collect statements
|
|
952
|
+
for (const arn of policyArns) {
|
|
953
|
+
const policyRecord = this.policies.get(arn);
|
|
954
|
+
if (policyRecord && policyRecord.Document && Array.isArray(policyRecord.Document.Statement)) {
|
|
955
|
+
for (const statement of policyRecord.Document.Statement) {
|
|
956
|
+
allStatements.push(statement);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return allStatements;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
evaluateAccess({ accessKeyId, action, resource, context }) {
|
|
965
|
+
const identity = this.resolveIdentity(accessKeyId);
|
|
966
|
+
if (!identity) return 'Deny';
|
|
967
|
+
|
|
968
|
+
const allStatements = this._collectPolicies(identity);
|
|
969
|
+
|
|
970
|
+
let allowed = false;
|
|
971
|
+
|
|
972
|
+
for (const statement of allStatements) {
|
|
973
|
+
// Normalize Action and Resource to arrays
|
|
974
|
+
const actions = Array.isArray(statement.Action) ? statement.Action : [statement.Action];
|
|
975
|
+
const resources = Array.isArray(statement.Resource) ? statement.Resource : [statement.Resource];
|
|
976
|
+
|
|
977
|
+
// Check if this statement matches the request
|
|
978
|
+
const actionMatches = actions.some(a => this._matchAction(a, action));
|
|
979
|
+
const resourceMatches = resources.some(r => this._matchResource(r, resource));
|
|
980
|
+
|
|
981
|
+
if (!actionMatches || !resourceMatches) continue;
|
|
982
|
+
|
|
983
|
+
// Check conditions if present
|
|
984
|
+
if (statement.Condition) {
|
|
985
|
+
if (!this._evaluateConditions(statement.Condition, context)) continue;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Statement matches - check effect
|
|
989
|
+
if (statement.Effect === 'Deny') {
|
|
990
|
+
return 'Deny'; // Explicit deny always wins
|
|
991
|
+
}
|
|
992
|
+
if (statement.Effect === 'Allow') {
|
|
993
|
+
allowed = true;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
return allowed ? 'Allow' : 'Deny';
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
_matchAction(pattern, action) {
|
|
1001
|
+
if (pattern === '*') return true;
|
|
1002
|
+
return this._wildcardMatch(pattern.toLowerCase(), action.toLowerCase());
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
_matchResource(pattern, resource) {
|
|
1006
|
+
if (pattern === '*') return true;
|
|
1007
|
+
return this._wildcardMatch(pattern, resource);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
_wildcardMatch(pattern, value) {
|
|
1011
|
+
let p = 0, v = 0, starP = -1, matchV = -1;
|
|
1012
|
+
|
|
1013
|
+
while (v < value.length) {
|
|
1014
|
+
if (p < pattern.length && (pattern[p] === '?' || pattern[p] === value[v])) {
|
|
1015
|
+
p++;
|
|
1016
|
+
v++;
|
|
1017
|
+
} else if (p < pattern.length && pattern[p] === '*') {
|
|
1018
|
+
starP = p;
|
|
1019
|
+
matchV = v;
|
|
1020
|
+
p++;
|
|
1021
|
+
} else if (starP !== -1) {
|
|
1022
|
+
p = starP + 1;
|
|
1023
|
+
matchV++;
|
|
1024
|
+
v = matchV;
|
|
1025
|
+
} else {
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
while (p < pattern.length && pattern[p] === '*') {
|
|
1031
|
+
p++;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
return p === pattern.length;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
_evaluateConditions(conditions, context) {
|
|
1038
|
+
for (const [operator, conditionMap] of Object.entries(conditions)) {
|
|
1039
|
+
for (const [key, value] of Object.entries(conditionMap)) {
|
|
1040
|
+
switch (operator) {
|
|
1041
|
+
case 'StringEquals':
|
|
1042
|
+
if (!this._evaluateStringEquals(key, value, context)) return false;
|
|
1043
|
+
break;
|
|
1044
|
+
case 'StringLike':
|
|
1045
|
+
if (!this._evaluateStringLike(key, value, context)) return false;
|
|
1046
|
+
break;
|
|
1047
|
+
case 'IpAddress':
|
|
1048
|
+
if (!this._evaluateIpAddress(key, value, context)) return false;
|
|
1049
|
+
break;
|
|
1050
|
+
default:
|
|
1051
|
+
// Unknown operator — skip (permissive)
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return true;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
_evaluateStringEquals(key, value, context) {
|
|
1060
|
+
const contextValue = context[key];
|
|
1061
|
+
if (contextValue === undefined) return false;
|
|
1062
|
+
if (Array.isArray(value)) return value.includes(contextValue);
|
|
1063
|
+
return contextValue === value;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
_evaluateStringLike(key, pattern, context) {
|
|
1067
|
+
const contextValue = context[key];
|
|
1068
|
+
if (contextValue === undefined) return false;
|
|
1069
|
+
if (Array.isArray(pattern)) {
|
|
1070
|
+
return pattern.some(p => this._wildcardMatch(p, contextValue));
|
|
1071
|
+
}
|
|
1072
|
+
return this._wildcardMatch(pattern, contextValue);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
_evaluateIpAddress(key, cidr, context) {
|
|
1076
|
+
const sourceIp = context[key];
|
|
1077
|
+
if (sourceIp === undefined) return false;
|
|
1078
|
+
if (Array.isArray(cidr)) {
|
|
1079
|
+
return cidr.some(c => this._cidrContains(c, sourceIp));
|
|
1080
|
+
}
|
|
1081
|
+
return this._cidrContains(cidr, sourceIp);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
_cidrContains(cidr, ip) {
|
|
1085
|
+
const [baseIp, prefixStr] = cidr.split('/');
|
|
1086
|
+
const prefix = parseInt(prefixStr, 10);
|
|
1087
|
+
if (isNaN(prefix) || prefix < 0 || prefix > 32) return false;
|
|
1088
|
+
const mask = prefix === 0 ? 0 : ((-1 << (32 - prefix)) >>> 0);
|
|
1089
|
+
const baseInt = this._ipToInt(baseIp);
|
|
1090
|
+
const ipInt = this._ipToInt(ip);
|
|
1091
|
+
return (ipInt & mask) === (baseInt & mask);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
_ipToInt(ip) {
|
|
1095
|
+
const parts = ip.split('.').map(Number);
|
|
1096
|
+
return ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
async _persist() {
|
|
1100
|
+
if (!fs.existsSync(DATA_DIR)) {
|
|
1101
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
for (const [mapName, fileName] of Object.entries(FILE_MAP)) {
|
|
1105
|
+
const filePath = path.join(DATA_DIR, fileName);
|
|
1106
|
+
const map = this[mapName];
|
|
1107
|
+
let data;
|
|
1108
|
+
|
|
1109
|
+
// Maps that store Sets as values need special serialization
|
|
1110
|
+
if (['userPolicies', 'groupPolicies', 'rolePolicies', 'groupMembers', 'userGroups'].includes(mapName)) {
|
|
1111
|
+
const obj = {};
|
|
1112
|
+
for (const [key, value] of map.entries()) {
|
|
1113
|
+
obj[key] = Array.from(value);
|
|
1114
|
+
}
|
|
1115
|
+
data = JSON.stringify(obj, null, 2);
|
|
1116
|
+
} else {
|
|
1117
|
+
// Maps that store objects as values (users, groups, roles, policies, accessKeys)
|
|
1118
|
+
const entries = Array.from(map.values());
|
|
1119
|
+
data = JSON.stringify(entries, null, 2);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
fs.writeFileSync(filePath, data, 'utf-8');
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
logger.debug('IAM state persisted to disk.');
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
async _load() {
|
|
1129
|
+
if (!fs.existsSync(DATA_DIR)) {
|
|
1130
|
+
logger.debug('IAM data directory not found, starting with empty state.');
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
for (const [mapName, fileName] of Object.entries(FILE_MAP)) {
|
|
1135
|
+
const filePath = path.join(DATA_DIR, fileName);
|
|
1136
|
+
|
|
1137
|
+
if (!fs.existsSync(filePath)) {
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
try {
|
|
1142
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
1143
|
+
const parsed = JSON.parse(raw);
|
|
1144
|
+
|
|
1145
|
+
if (['userPolicies', 'groupPolicies', 'rolePolicies', 'groupMembers', 'userGroups'].includes(mapName)) {
|
|
1146
|
+
// These are objects where keys map to arrays (restored as Sets)
|
|
1147
|
+
this[mapName] = new Map();
|
|
1148
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
1149
|
+
this[mapName].set(key, new Set(value));
|
|
1150
|
+
}
|
|
1151
|
+
} else {
|
|
1152
|
+
// These are arrays of records, keyed by their primary identifier
|
|
1153
|
+
this[mapName] = new Map();
|
|
1154
|
+
const keyField = this._getKeyField(mapName);
|
|
1155
|
+
for (const record of parsed) {
|
|
1156
|
+
this[mapName].set(record[keyField], record);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
} catch (err) {
|
|
1160
|
+
logger.warn(`Failed to load IAM data from ${fileName}: ${err.message}`);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
logger.debug('IAM state loaded from disk.');
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
_getKeyField(mapName) {
|
|
1168
|
+
switch (mapName) {
|
|
1169
|
+
case 'users': return 'UserName';
|
|
1170
|
+
case 'groups': return 'GroupName';
|
|
1171
|
+
case 'roles': return 'RoleName';
|
|
1172
|
+
case 'policies': return 'Arn';
|
|
1173
|
+
case 'accessKeys': return 'AccessKeyId';
|
|
1174
|
+
default: return 'id';
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
async reset() {
|
|
1179
|
+
this.users = new Map();
|
|
1180
|
+
this.groups = new Map();
|
|
1181
|
+
this.roles = new Map();
|
|
1182
|
+
this.policies = new Map();
|
|
1183
|
+
this.accessKeys = new Map();
|
|
1184
|
+
this.userPolicies = new Map();
|
|
1185
|
+
this.groupPolicies = new Map();
|
|
1186
|
+
this.rolePolicies = new Map();
|
|
1187
|
+
this.groupMembers = new Map();
|
|
1188
|
+
this.userGroups = new Map();
|
|
1189
|
+
|
|
1190
|
+
await this._provisionFromConfig();
|
|
1191
|
+
await this._persist();
|
|
1192
|
+
|
|
1193
|
+
logger.debug('IAM state reset and re-provisioned from config.');
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// --- User Management ---
|
|
1197
|
+
|
|
1198
|
+
async createUser(params) {
|
|
1199
|
+
const { UserName, Path = '/' } = params || {};
|
|
1200
|
+
|
|
1201
|
+
if (!UserName) {
|
|
1202
|
+
const err = new Error('1 validation error detected: Value at \'userName\' failed to satisfy constraint: Member must not be null');
|
|
1203
|
+
err.code = 'ValidationError';
|
|
1204
|
+
err.statusCode = 400;
|
|
1205
|
+
throw err;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (this.users.has(UserName)) {
|
|
1209
|
+
const err = new Error(`User with name ${UserName} already exists.`);
|
|
1210
|
+
err.code = 'EntityAlreadyExists';
|
|
1211
|
+
err.statusCode = 409;
|
|
1212
|
+
throw err;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const UserId = 'AIDA' + crypto.randomUUID().replace(/-/g, '').substring(0, 21).toUpperCase();
|
|
1216
|
+
const Arn = `arn:aws:iam::123456789012:user${Path}${UserName}`;
|
|
1217
|
+
const CreateDate = new Date().toISOString();
|
|
1218
|
+
|
|
1219
|
+
const user = { UserName, UserId, Arn, Path, CreateDate };
|
|
1220
|
+
|
|
1221
|
+
this.users.set(UserName, user);
|
|
1222
|
+
this.userPolicies.set(UserName, new Set());
|
|
1223
|
+
this.userGroups.set(UserName, new Set());
|
|
1224
|
+
|
|
1225
|
+
await this._persist();
|
|
1226
|
+
|
|
1227
|
+
return { User: user };
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
async getUser(params) {
|
|
1231
|
+
const { UserName } = params || {};
|
|
1232
|
+
|
|
1233
|
+
if (!this.users.has(UserName)) {
|
|
1234
|
+
const err = new Error(`The user with name ${UserName} cannot be found.`);
|
|
1235
|
+
err.code = 'NoSuchEntity';
|
|
1236
|
+
err.statusCode = 404;
|
|
1237
|
+
throw err;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
return { User: this.users.get(UserName) };
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
async listUsers(params) {
|
|
1244
|
+
const { PathPrefix = '/' } = params || {};
|
|
1245
|
+
|
|
1246
|
+
const filteredUsers = [];
|
|
1247
|
+
for (const user of this.users.values()) {
|
|
1248
|
+
if (user.Path.startsWith(PathPrefix)) {
|
|
1249
|
+
filteredUsers.push(user);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
return { Users: filteredUsers };
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
async deleteUser(params) {
|
|
1257
|
+
const { UserName } = params || {};
|
|
1258
|
+
|
|
1259
|
+
if (!this.users.has(UserName)) {
|
|
1260
|
+
const err = new Error(`The user with name ${UserName} cannot be found.`);
|
|
1261
|
+
err.code = 'NoSuchEntity';
|
|
1262
|
+
err.statusCode = 404;
|
|
1263
|
+
throw err;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Remove all access keys for this user
|
|
1267
|
+
for (const [keyId, keyRecord] of this.accessKeys.entries()) {
|
|
1268
|
+
if (keyRecord.UserName === UserName) {
|
|
1269
|
+
this.accessKeys.delete(keyId);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// Remove user from all groups
|
|
1274
|
+
const groups = this.userGroups.get(UserName);
|
|
1275
|
+
if (groups) {
|
|
1276
|
+
for (const groupName of groups) {
|
|
1277
|
+
const members = this.groupMembers.get(groupName);
|
|
1278
|
+
if (members) {
|
|
1279
|
+
members.delete(UserName);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// Decrement attachment count for all policies attached to this user
|
|
1285
|
+
const userPolicies = this.userPolicies.get(UserName);
|
|
1286
|
+
if (userPolicies) {
|
|
1287
|
+
for (const policyArn of userPolicies) {
|
|
1288
|
+
const policyRecord = this.policies.get(policyArn);
|
|
1289
|
+
if (policyRecord) {
|
|
1290
|
+
policyRecord.AttachmentCount = Math.max(0, policyRecord.AttachmentCount - 1);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Delete user records
|
|
1296
|
+
this.users.delete(UserName);
|
|
1297
|
+
this.userPolicies.delete(UserName);
|
|
1298
|
+
this.userGroups.delete(UserName);
|
|
1299
|
+
|
|
1300
|
+
await this._persist();
|
|
1301
|
+
|
|
1302
|
+
return {}; // Return empty object for successful delete
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
async _provisionFromConfig() {
|
|
1306
|
+
const iamConfig = this.config?.iam;
|
|
1307
|
+
if (!iamConfig) return;
|
|
1308
|
+
|
|
1309
|
+
// 1. Create policies first (other resources reference them)
|
|
1310
|
+
if (Array.isArray(iamConfig.policies)) {
|
|
1311
|
+
for (const policyDef of iamConfig.policies) {
|
|
1312
|
+
try {
|
|
1313
|
+
await this.createPolicy({
|
|
1314
|
+
PolicyName: policyDef.PolicyName,
|
|
1315
|
+
Path: policyDef.Path || '/',
|
|
1316
|
+
PolicyDocument: policyDef.Document
|
|
1317
|
+
});
|
|
1318
|
+
logger.debug(`IAM Config: Created policy ${policyDef.PolicyName}`);
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
// Skip if already exists (from persistence)
|
|
1321
|
+
if (err.code !== 'EntityAlreadyExists') {
|
|
1322
|
+
logger.warn(`IAM Config: Failed to create policy ${policyDef.PolicyName}: ${err.message}`);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// 2. Create users, attach policies, generate keys
|
|
1329
|
+
if (Array.isArray(iamConfig.users)) {
|
|
1330
|
+
for (const userDef of iamConfig.users) {
|
|
1331
|
+
try {
|
|
1332
|
+
await this.createUser({
|
|
1333
|
+
UserName: userDef.UserName,
|
|
1334
|
+
Path: userDef.Path || '/'
|
|
1335
|
+
});
|
|
1336
|
+
logger.debug(`IAM Config: Created user ${userDef.UserName}`);
|
|
1337
|
+
|
|
1338
|
+
// Attach policies
|
|
1339
|
+
if (Array.isArray(userDef.Policies)) {
|
|
1340
|
+
for (const policyArn of userDef.Policies) {
|
|
1341
|
+
try {
|
|
1342
|
+
await this.attachUserPolicy({ UserName: userDef.UserName, PolicyArn: policyArn });
|
|
1343
|
+
} catch (err) {
|
|
1344
|
+
logger.warn(`IAM Config: Failed to attach policy ${policyArn} to user ${userDef.UserName}: ${err.message}`);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Generate access keys if requested
|
|
1350
|
+
if (userDef.generateAccessKeys) {
|
|
1351
|
+
const key = await this.createAccessKey({ UserName: userDef.UserName });
|
|
1352
|
+
logger.info(`🔑 IAM: Generated AccessKey for ${userDef.UserName}: ${key.AccessKeyId}`);
|
|
1353
|
+
}
|
|
1354
|
+
} catch (err) {
|
|
1355
|
+
if (err.code !== 'EntityAlreadyExists') {
|
|
1356
|
+
logger.warn(`IAM Config: Failed to create user ${userDef.UserName}: ${err.message}`);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// 3. Create groups, add members, attach policies
|
|
1363
|
+
if (Array.isArray(iamConfig.groups)) {
|
|
1364
|
+
for (const groupDef of iamConfig.groups) {
|
|
1365
|
+
try {
|
|
1366
|
+
await this.createGroup({
|
|
1367
|
+
GroupName: groupDef.GroupName,
|
|
1368
|
+
Path: groupDef.Path || '/'
|
|
1369
|
+
});
|
|
1370
|
+
logger.debug(`IAM Config: Created group ${groupDef.GroupName}`);
|
|
1371
|
+
|
|
1372
|
+
// Add members
|
|
1373
|
+
if (Array.isArray(groupDef.Members)) {
|
|
1374
|
+
for (const userName of groupDef.Members) {
|
|
1375
|
+
try {
|
|
1376
|
+
await this.addUserToGroup({ GroupName: groupDef.GroupName, UserName: userName });
|
|
1377
|
+
} catch (err) {
|
|
1378
|
+
logger.warn(`IAM Config: Failed to add ${userName} to group ${groupDef.GroupName}: ${err.message}`);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// Attach policies
|
|
1384
|
+
if (Array.isArray(groupDef.Policies)) {
|
|
1385
|
+
for (const policyArn of groupDef.Policies) {
|
|
1386
|
+
try {
|
|
1387
|
+
await this.attachGroupPolicy({ GroupName: groupDef.GroupName, PolicyArn: policyArn });
|
|
1388
|
+
} catch (err) {
|
|
1389
|
+
logger.warn(`IAM Config: Failed to attach policy ${policyArn} to group ${groupDef.GroupName}: ${err.message}`);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
if (err.code !== 'EntityAlreadyExists') {
|
|
1395
|
+
logger.warn(`IAM Config: Failed to create group ${groupDef.GroupName}: ${err.message}`);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// 4. Create roles, attach policies
|
|
1402
|
+
if (Array.isArray(iamConfig.roles)) {
|
|
1403
|
+
for (const roleDef of iamConfig.roles) {
|
|
1404
|
+
try {
|
|
1405
|
+
await this.createRole({
|
|
1406
|
+
RoleName: roleDef.RoleName,
|
|
1407
|
+
Path: roleDef.Path || '/',
|
|
1408
|
+
AssumeRolePolicyDocument: roleDef.AssumeRolePolicyDocument
|
|
1409
|
+
});
|
|
1410
|
+
logger.debug(`IAM Config: Created role ${roleDef.RoleName}`);
|
|
1411
|
+
|
|
1412
|
+
// Attach policies
|
|
1413
|
+
if (Array.isArray(roleDef.Policies)) {
|
|
1414
|
+
for (const policyArn of roleDef.Policies) {
|
|
1415
|
+
try {
|
|
1416
|
+
await this.attachRolePolicy({ RoleName: roleDef.RoleName, PolicyArn: policyArn });
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
logger.warn(`IAM Config: Failed to attach policy ${policyArn} to role ${roleDef.RoleName}: ${err.message}`);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
} catch (err) {
|
|
1423
|
+
if (err.code !== 'EntityAlreadyExists') {
|
|
1424
|
+
logger.warn(`IAM Config: Failed to create role ${roleDef.RoleName}: ${err.message}`);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
module.exports = IAMSimulator;
|