@gugananuvem/aws-local-simulator 1.0.35 → 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.
Files changed (149) hide show
  1. package/README.md +662 -658
  2. package/package.json +10 -3
  3. package/src/config/default-config.js +71 -0
  4. package/src/config/env-loader.js +4 -2
  5. package/src/middleware/authorization.js +271 -0
  6. package/src/middleware/cloudtrail-logger.js +333 -0
  7. package/src/middleware/error-handler.js +138 -0
  8. package/src/middleware/index.js +79 -0
  9. package/src/middleware/policy-enforcement.js +407 -0
  10. package/src/server.js +74 -4
  11. package/src/services/acm/index.js +66 -0
  12. package/src/services/acm/models/certificate.js +202 -0
  13. package/src/services/acm/server.js +224 -0
  14. package/src/services/acm/simulator.js +293 -0
  15. package/src/services/alb/index.js +104 -0
  16. package/src/services/alb/models/loadBalancer.js +88 -0
  17. package/src/services/alb/models/targetGroup.js +128 -0
  18. package/src/services/alb/server.js +160 -0
  19. package/src/services/alb/simulator.js +262 -0
  20. package/src/services/apigateway/index.js +22 -0
  21. package/src/services/apigateway/server.js +247 -35
  22. package/src/services/apigateway/simulator.js +780 -26
  23. package/src/services/asg/index.js +100 -0
  24. package/src/services/asg/models/autoScalingGroup.js +110 -0
  25. package/src/services/asg/models/launchTemplate.js +41 -0
  26. package/src/services/asg/models/scalingPolicy.js +51 -0
  27. package/src/services/asg/server.js +236 -0
  28. package/src/services/asg/simulator.js +410 -0
  29. package/src/services/aurora/cluster-manager.js +731 -0
  30. package/src/services/aurora/data-api.js +514 -0
  31. package/src/services/aurora/db-engine.js +270 -0
  32. package/src/services/aurora/index.js +159 -0
  33. package/src/services/aurora/rds-api.js +189 -0
  34. package/src/services/aurora/server.js +213 -0
  35. package/src/services/aurora/sql-translator.js +334 -0
  36. package/src/services/cloudformation/index.js +2 -2
  37. package/src/services/cloudformation/server.js +1 -0
  38. package/src/services/cloudfront/cache-engine.js +225 -0
  39. package/src/services/cloudfront/geo-restrictions.js +377 -0
  40. package/src/services/cloudfront/index.js +220 -0
  41. package/src/services/cloudfront/models/behavior.js +158 -0
  42. package/src/services/cloudfront/models/distribution.js +284 -0
  43. package/src/services/cloudfront/models/index.js +99 -0
  44. package/src/services/cloudfront/models/invalidation.js +109 -0
  45. package/src/services/cloudfront/models/origin.js +155 -0
  46. package/src/services/cloudfront/origin-router.js +552 -0
  47. package/src/services/cloudfront/origin-validator.js +398 -0
  48. package/src/services/cloudfront/server.js +512 -0
  49. package/src/services/cloudfront/signed-url-generator.js +377 -0
  50. package/src/services/cloudfront/simulator.js +425 -0
  51. package/src/services/cloudfront/store.js +326 -0
  52. package/src/services/cloudshell/env-builder.js +27 -0
  53. package/src/services/cloudshell/index.js +48 -0
  54. package/src/services/cloudshell/routes.js +60 -0
  55. package/src/services/cloudshell/session-manager.js +226 -0
  56. package/src/services/cloudshell/static/.gitkeep +0 -0
  57. package/src/services/cloudshell/static/index.html +406 -0
  58. package/src/services/cloudshell/websocket-handler.js +107 -0
  59. package/src/services/cloudtrail/index.js +2 -3
  60. package/src/services/cloudtrail/server.js +68 -0
  61. package/src/services/cognito/server.js +4 -1
  62. package/src/services/dynamodb/server.js +4 -1
  63. package/src/services/dynamodb/simulator.js +2 -1
  64. package/src/services/ec2/index.js +101 -0
  65. package/src/services/ec2/instanceServer.js +311 -0
  66. package/src/services/ec2/models/ami.js +39 -0
  67. package/src/services/ec2/models/instance.js +98 -0
  68. package/src/services/ec2/models/networkInterface.js +74 -0
  69. package/src/services/ec2/models/securityGroup.js +71 -0
  70. package/src/services/ec2/server.js +350 -0
  71. package/src/services/ec2/simulator.js +436 -0
  72. package/src/services/eventbridge/server.js +36 -0
  73. package/src/services/eventbridge/simulator.js +230 -45
  74. package/src/services/iam/index.js +65 -0
  75. package/src/services/iam/middleware.js +99 -0
  76. package/src/services/iam/server.js +331 -0
  77. package/src/services/iam/simulator.js +1432 -0
  78. package/src/services/index.js +28 -0
  79. package/src/services/kinesis-analytics/index.js +105 -0
  80. package/src/services/kinesis-analytics/models/application.js +325 -0
  81. package/src/services/kinesis-analytics/models/index.js +12 -0
  82. package/src/services/kinesis-analytics/server.js +250 -0
  83. package/src/services/kinesis-analytics/simulator.js +407 -0
  84. package/src/services/kinesis-analytics/store.js +176 -0
  85. package/src/services/kinesis-data-streams/index.js +106 -0
  86. package/src/services/kinesis-data-streams/models/index.js +12 -0
  87. package/src/services/kinesis-data-streams/models/record.js +70 -0
  88. package/src/services/kinesis-data-streams/models/stream.js +280 -0
  89. package/src/services/kinesis-data-streams/server.js +255 -0
  90. package/src/services/kinesis-data-streams/simulator.js +514 -0
  91. package/src/services/kinesis-data-streams/store.js +163 -0
  92. package/src/services/kinesis-firehose/index.js +106 -0
  93. package/src/services/kinesis-firehose/models/delivery-stream.js +265 -0
  94. package/src/services/kinesis-firehose/models/index.js +10 -0
  95. package/src/services/kinesis-firehose/server.js +215 -0
  96. package/src/services/kinesis-firehose/simulator.js +388 -0
  97. package/src/services/kinesis-firehose/store.js +163 -0
  98. package/src/services/kms/server.js +4 -1
  99. package/src/services/lambda/index.js +26 -0
  100. package/src/services/lambda/server.js +12 -1
  101. package/src/services/lambda/simulator.js +4 -0
  102. package/src/services/opensearch/index.js +105 -0
  103. package/src/services/opensearch/models/document.js +115 -0
  104. package/src/services/opensearch/models/index.js +73 -0
  105. package/src/services/opensearch/server.js +310 -0
  106. package/src/services/opensearch/simulator.js +605 -0
  107. package/src/services/opensearch/store.js +162 -0
  108. package/src/services/organizations/control-tower-simulator.js +321 -0
  109. package/src/services/organizations/control-tower-store.js +182 -0
  110. package/src/services/organizations/iam-integration.js +413 -0
  111. package/src/services/organizations/index.js +172 -0
  112. package/src/services/organizations/models/index.js +13 -0
  113. package/src/services/organizations/models/invitation.js +48 -0
  114. package/src/services/organizations/models/organization.js +566 -0
  115. package/src/services/organizations/models/role.js +244 -0
  116. package/src/services/organizations/models/scp.js +131 -0
  117. package/src/services/organizations/server.js +736 -0
  118. package/src/services/organizations/simulator.js +593 -0
  119. package/src/services/organizations/store.js +235 -0
  120. package/src/services/organizations/vpc-integration.js +453 -0
  121. package/src/services/route53/index.js +67 -0
  122. package/src/services/route53/models/healthCheck.js +101 -0
  123. package/src/services/route53/models/hostedZone.js +37 -0
  124. package/src/services/route53/models/resourceRecord.js +106 -0
  125. package/src/services/route53/server.js +841 -0
  126. package/src/services/route53/simulator.js +350 -0
  127. package/src/services/s3/index.js +62 -0
  128. package/src/services/s3/presigned-url-generator.js +337 -0
  129. package/src/services/s3/server.js +231 -0
  130. package/src/services/s3/simulator.js +551 -4
  131. package/src/services/sns/index.js +31 -0
  132. package/src/services/sns/server.js +9 -0
  133. package/src/services/sqs/index.js +38 -0
  134. package/src/services/sqs/server.js +57 -4
  135. package/src/services/sqs/simulator.js +423 -33
  136. package/src/services/sts/simulator.js +209 -17
  137. package/src/services/vpc/README.md +459 -0
  138. package/src/services/vpc/ec2ResponseFormatter.js +431 -0
  139. package/src/services/vpc/index.js +90 -0
  140. package/src/services/vpc/models/internetGateway.js +71 -0
  141. package/src/services/vpc/models/routeTable.js +160 -0
  142. package/src/services/vpc/models/subnet.js +48 -0
  143. package/src/services/vpc/models/vpc.js +37 -0
  144. package/src/services/vpc/models/vpcEndpoint.js +100 -0
  145. package/src/services/vpc/models/vpcGateway.js +134 -0
  146. package/src/services/vpc/server.js +836 -0
  147. package/src/services/vpc/simulator.js +613 -0
  148. package/src/utils/policy-cache.js +215 -0
  149. package/aws-config +0 -154
@@ -0,0 +1,593 @@
1
+ /**
2
+ * AWS Organizations Simulator
3
+ * Gerencia organização, contas, OUs, policies e convites (handshakes).
4
+ *
5
+ * Persistência: um arquivo JSON por coleção em <dataDir>/organizations,
6
+ * no mesmo padrão do IAM Simulator (`_persist()`/`_load()`).
7
+ */
8
+
9
+ const Organization = require('./models/organization');
10
+ const Invitation = require('./models/invitation');
11
+ const OrganizationRole = require('./models/role');
12
+ const LocalStore = require('../../utils/local-store');
13
+ const logger = require('../../utils/logger');
14
+ const path = require('path');
15
+ const { CloudTrailAudit } = require('../../utils/cloudtrail-audit');
16
+
17
+ // Entidades persistidas (uma coleção por arquivo JSON).
18
+ const FILE_MAP = {
19
+ metadata: '__organization__.json',
20
+ accounts: '__accounts__.json',
21
+ ous: '__ous__.json',
22
+ policies: '__policies__.json',
23
+ roles: '__roles__.json',
24
+ invitations: '__invitations__.json',
25
+ serviceAccess: '__service-access__.json',
26
+ };
27
+
28
+ class OrganizationsSimulator {
29
+ constructor(config) {
30
+ this.config = config;
31
+ const dataDir = process.env.AWS_LOCAL_SIMULATOR_DATA_DIR || config.dataDir || './.aws-local-simulator-data';
32
+ this.dataDir = path.join(dataDir, 'organizations');
33
+
34
+ this.store = new LocalStore(this.dataDir);
35
+ this.organization = null;
36
+ this.roles = new Map();
37
+ this.invitations = new Map();
38
+ this.audit = new CloudTrailAudit('organizations.amazonaws.com');
39
+
40
+ // Mutexes para thread safety
41
+ this._orgLock = Promise.resolve();
42
+ }
43
+
44
+ /**
45
+ * Executa operação com lock
46
+ */
47
+ async _withOrgLock(fn) {
48
+ const prev = this._orgLock;
49
+ const next = prev.then(() => fn());
50
+ this._orgLock = next.catch(() => {});
51
+ return next;
52
+ }
53
+
54
+ async initialize() {
55
+ logger.debug('Inicializando Organizations Simulator...');
56
+
57
+ await this._load();
58
+
59
+ logger.debug(
60
+ `✅ Organizations Simulator inicializado — org ${this.organization.id}, ` +
61
+ `${this.organization.accounts.size} contas, ${this.organization.ous.size} OUs, ` +
62
+ `${this.invitations.size} convites`
63
+ );
64
+ }
65
+
66
+ // ============ Persistência (padrão IAM) ============
67
+
68
+ async _persist() {
69
+ const snapshot = this.organization.toSnapshot();
70
+
71
+ this.store.write(FILE_MAP.metadata, {
72
+ id: snapshot.id,
73
+ arn: snapshot.arn,
74
+ rootId: snapshot.rootId,
75
+ masterAccountId: snapshot.masterAccountId,
76
+ masterAccountArn: snapshot.masterAccountArn,
77
+ masterAccountEmail: snapshot.masterAccountEmail,
78
+ masterAccountName: snapshot.masterAccountName,
79
+ createdAt: snapshot.createdAt,
80
+ status: snapshot.status,
81
+ featureSet: snapshot.featureSet,
82
+ policyTypes: snapshot.policyTypes
83
+ });
84
+ this.store.write(FILE_MAP.accounts, snapshot.accounts);
85
+ this.store.write(FILE_MAP.ous, snapshot.ous);
86
+ this.store.write(FILE_MAP.policies, snapshot.policies);
87
+ this.store.write(FILE_MAP.roles, Array.from(this.roles.values()).map(r => r.toJSON()));
88
+ this.store.write(FILE_MAP.invitations, Array.from(this.invitations.values()));
89
+ this.store.write(FILE_MAP.serviceAccess, this.organization.listServiceAccess());
90
+ }
91
+
92
+ async _load() {
93
+ const metadata = this.store.read(FILE_MAP.metadata);
94
+ const accounts = this.store.read(FILE_MAP.accounts);
95
+ const ous = this.store.read(FILE_MAP.ous);
96
+ const policies = this.store.read(FILE_MAP.policies);
97
+ const serviceAccess = this.store.read(FILE_MAP.serviceAccess);
98
+
99
+ const snapshot = {
100
+ ...(metadata && metadata.id ? metadata : {}),
101
+ accounts: Array.isArray(accounts) ? accounts : [],
102
+ ous: Array.isArray(ous) ? ous : [],
103
+ policies: Array.isArray(policies) ? policies : [],
104
+ serviceAccess: Array.isArray(serviceAccess) ? serviceAccess : []
105
+ };
106
+
107
+ const isNewOrganization = !snapshot.id;
108
+ this.organization = Organization.fromSnapshot(snapshot);
109
+ if (isNewOrganization) await this._persist();
110
+
111
+ this._loadRoles();
112
+ this._loadInvitations();
113
+ }
114
+
115
+ /**
116
+ * Compatibilidade: mantém o nome antigo usado por chamadores existentes.
117
+ */
118
+ persistOrganization() {
119
+ return this._persist();
120
+ }
121
+
122
+ _loadRoles() {
123
+ const savedRoles = this.store.read(FILE_MAP.roles);
124
+ this.roles = new Map();
125
+
126
+ if (!Array.isArray(savedRoles)) return;
127
+
128
+ for (const roleData of savedRoles) {
129
+ const role = new OrganizationRole(roleData.name, roleData.trustPolicy, {
130
+ accountId: roleData.accountId
131
+ });
132
+ if (roleData.id) role.id = roleData.id;
133
+ this.roles.set(role.id, role);
134
+ }
135
+ }
136
+
137
+ _loadInvitations() {
138
+ const saved = this.store.read(FILE_MAP.invitations);
139
+ this.invitations = new Map();
140
+
141
+ if (!Array.isArray(saved)) return;
142
+
143
+ for (const data of saved) {
144
+ this.invitations.set(data.id, new Invitation(data));
145
+ }
146
+ }
147
+
148
+ // ============ Organizations ============
149
+
150
+ describeOrganization() {
151
+ return this.organization.describe();
152
+ }
153
+
154
+ listRoots() {
155
+ return { Roots: [this.getRoot()] };
156
+ }
157
+
158
+ /** Raiz da organização no formato da API (com os policy types habilitados). */
159
+ getRoot() {
160
+ return {
161
+ Id: this.organization.rootId,
162
+ Arn: `arn:aws:organizations::123456789012:ou/o-${this.organization.id}/${this.organization.rootId}`,
163
+ Name: 'Root',
164
+ PolicyTypes: this.organization.listPolicyTypes()
165
+ };
166
+ }
167
+
168
+ createOrganizationalUnit(parentId, name) {
169
+ const ou = this.organization.createOrganizationalUnit(parentId, name);
170
+
171
+ this.persistOrganization();
172
+ this.audit.record({
173
+ eventName: 'CreateOrganizationalUnit',
174
+ readOnly: false,
175
+ resources: [{ ARN: ou.arn, type: 'AWS::Organizations::OrganizationalUnit' }],
176
+ requestParameters: { parentId, name }
177
+ });
178
+
179
+ logger.info(`✅ OU criada: ${name}`);
180
+
181
+ return { OrganizationalUnit: ou };
182
+ }
183
+
184
+ describeOrganizationalUnit(ouId) {
185
+ const ou = this.organization.getOrganizationalUnit(ouId);
186
+ if (!ou) throw new Error(`OU '${ouId}' not found`);
187
+
188
+ return { OrganizationalUnit: ou };
189
+ }
190
+
191
+ listOrganizationalUnitsForParent(parentId) {
192
+ const parent = this.organization.getOrganizationalUnit(parentId);
193
+ if (!parent) throw new Error(`Parent OU '${parentId}' not found`);
194
+
195
+ return { OrganizationalUnits: this.organization.listOrganizationalUnitsForParent(parentId) };
196
+ }
197
+
198
+ deleteOrganizationalUnit(ouId) {
199
+ this.organization.deleteOrganizationalUnit(ouId);
200
+ this.persistOrganization();
201
+
202
+ logger.info(`✅ OU removida: ${ouId}`);
203
+ return {};
204
+ }
205
+
206
+ // ============ Contas ============
207
+
208
+ createAccount(email, accountName, config = {}) {
209
+ const account = this.organization.createAccount(email, accountName, {
210
+ parentOUId: config.parentOUId || this.organization.rootId,
211
+ roleName: config.roleName,
212
+ tags: config.tags
213
+ });
214
+
215
+ this.persistOrganization();
216
+ this.audit.record({
217
+ eventName: 'CreateAccount',
218
+ readOnly: false,
219
+ resources: [{ ARN: account.arn, type: 'AWS::Organizations::Account' }],
220
+ requestParameters: { email, accountName }
221
+ });
222
+
223
+ logger.info(`✅ Conta criada: ${email}`);
224
+
225
+ return {
226
+ CreateAccountStatus: {
227
+ Id: `car-${account.id}`,
228
+ AccountName: accountName,
229
+ State: 'SUCCEEDED',
230
+ RequestedTimestamp: Math.floor(Date.now() / 1000),
231
+ CompletedTimestamp: Math.floor(Date.now() / 1000),
232
+ AccountId: account.id
233
+ }
234
+ };
235
+ }
236
+
237
+ listAccounts() {
238
+ return { Accounts: this.organization.listAccounts() };
239
+ }
240
+
241
+ listAccountsForParent(parentId) {
242
+ return { Accounts: this.organization.listAccountsForParent(parentId) };
243
+ }
244
+
245
+ describeAccount(accountId) {
246
+ const account = this.organization.getAccount(accountId);
247
+ if (!account) throw new Error(`Account '${accountId}' not found`);
248
+
249
+ return { Account: account };
250
+ }
251
+
252
+ moveAccount(accountId, sourceParentId, destinationParentId) {
253
+ const account = this.organization.moveAccount(accountId, destinationParentId);
254
+
255
+ this.persistOrganization();
256
+ this.audit.record({
257
+ eventName: 'MoveAccount',
258
+ readOnly: false,
259
+ resources: [{ ARN: account.arn, type: 'AWS::Organizations::Account' }],
260
+ requestParameters: { accountId, destinationParentId }
261
+ });
262
+
263
+ logger.info(`✅ Conta movida: ${accountId} -> ${destinationParentId}`);
264
+
265
+ return {};
266
+ }
267
+
268
+ removeAccountFromOrganization(accountId) {
269
+ const account = this.organization.removeAccount(accountId);
270
+
271
+ this.persistOrganization();
272
+ this.audit.record({
273
+ eventName: 'RemoveAccountFromOrganization',
274
+ readOnly: false,
275
+ resources: [{ ARN: account.arn, type: 'AWS::Organizations::Account' }],
276
+ requestParameters: { accountId }
277
+ });
278
+
279
+ return {};
280
+ }
281
+
282
+ // ============ Policies ============
283
+
284
+ createPolicy(name, document, type = 'SERVICE_CONTROL_POLICY') {
285
+ const policy = this.organization.createPolicy(name, document, type);
286
+
287
+ this.persistOrganization();
288
+ this.audit.record({
289
+ eventName: 'CreatePolicy',
290
+ readOnly: false,
291
+ resources: [{ ARN: policy.arn, type: 'AWS::Organizations::Policy' }],
292
+ requestParameters: { name, type }
293
+ });
294
+
295
+ logger.info(`✅ Policy criada: ${name}`);
296
+
297
+ return { Policy: { PolicySummary: policy, Content: JSON.stringify(policy.document) } };
298
+ }
299
+
300
+ describePolicy(policyId) {
301
+ const policy = this.organization.getPolicy(policyId);
302
+ if (!policy) throw new Error(`Policy '${policyId}' not found`);
303
+
304
+ return {
305
+ Policy: {
306
+ PolicySummary: policy,
307
+ Content: JSON.stringify(policy.document)
308
+ }
309
+ };
310
+ }
311
+
312
+ listPolicies(type = 'SERVICE_CONTROL_POLICY') {
313
+ const policies = this.organization.listPolicies(type);
314
+
315
+ return { Policies: policies.map(p => ({ PolicySummary: p })) };
316
+ }
317
+
318
+ /** Lista os policy types habilitados na raiz. */
319
+ listPolicyTypes() {
320
+ return { PolicyTypes: this.organization.listPolicyTypes() };
321
+ }
322
+
323
+ /** Habilita um policy type (EnablePolicyType). */
324
+ enablePolicyType(type) {
325
+ const result = this.organization.enablePolicyType(type);
326
+
327
+ this.persistOrganization();
328
+ this.audit.record({
329
+ eventName: 'EnablePolicyType',
330
+ readOnly: false,
331
+ resources: [{ ARN: this.getRoot().Arn, type: 'AWS::Organizations::Root' }],
332
+ requestParameters: { policyType: type }
333
+ });
334
+
335
+ logger.info(`✅ Policy type habilitado: ${type}`);
336
+
337
+ return result;
338
+ }
339
+
340
+ /** Desabilita um policy type (DisablePolicyType). */
341
+ disablePolicyType(type) {
342
+ const result = this.organization.disablePolicyType(type);
343
+
344
+ this.persistOrganization();
345
+ this.audit.record({
346
+ eventName: 'DisablePolicyType',
347
+ readOnly: false,
348
+ resources: [{ ARN: this.getRoot().Arn, type: 'AWS::Organizations::Root' }],
349
+ requestParameters: { policyType: type }
350
+ });
351
+
352
+ logger.info(`✅ Policy type desabilitado: ${type}`);
353
+
354
+ return result;
355
+ }
356
+
357
+ /** Lista os serviços com trusted access habilitado. */
358
+ listServiceAccess() {
359
+ return { EnabledServicePrincipals: this.organization.listServiceAccess() };
360
+ }
361
+
362
+ /** Habilita trusted access de um serviço (EnableAWSServiceAccess). */
363
+ enableServiceAccess(principal) {
364
+ const result = this.organization.enableServiceAccess(principal);
365
+
366
+ this.persistOrganization();
367
+ this.audit.record({
368
+ eventName: 'EnableAWSServiceAccess',
369
+ readOnly: false,
370
+ resources: [{ ARN: `arn:aws:organizations::123456789012:organization/o-${this.organization.id}`, type: 'AWS::Organizations::Organization' }],
371
+ requestParameters: { servicePrincipal: principal }
372
+ });
373
+
374
+ logger.info(`✅ Trusted access habilitado: ${principal}`);
375
+
376
+ return result;
377
+ }
378
+
379
+ /** Desabilita trusted access de um serviço (DisableAWSServiceAccess). */
380
+ disableServiceAccess(principal) {
381
+ const result = this.organization.disableServiceAccess(principal);
382
+
383
+ this.persistOrganization();
384
+ this.audit.record({
385
+ eventName: 'DisableAWSServiceAccess',
386
+ readOnly: false,
387
+ resources: [{ ARN: `arn:aws:organizations::123456789012:organization/o-${this.organization.id}`, type: 'AWS::Organizations::Organization' }],
388
+ requestParameters: { servicePrincipal: principal }
389
+ });
390
+
391
+ logger.info(`✅ Trusted access desabilitado: ${principal}`);
392
+
393
+ return result;
394
+ }
395
+
396
+ deletePolicy(policyId) {
397
+ const policy = this.organization.getPolicy(policyId);
398
+ if (!policy) throw new Error(`Policy '${policyId}' not found`);
399
+
400
+ this.organization.policies.delete(policyId);
401
+ this.organization.policyTargets.delete(policyId);
402
+ this.persistOrganization();
403
+
404
+ return {};
405
+ }
406
+
407
+ attachPolicy(policyId, targetId) {
408
+ const policy = this.organization.attachPolicy(policyId, targetId);
409
+
410
+ this.persistOrganization();
411
+ this.audit.record({
412
+ eventName: 'AttachPolicy',
413
+ readOnly: false,
414
+ resources: [{ ARN: policy.arn, type: 'AWS::Organizations::Policy' }],
415
+ requestParameters: { policyId, targetId }
416
+ });
417
+
418
+ logger.info(`✅ Policy anexada: ${policyId} -> ${targetId}`);
419
+
420
+ return {};
421
+ }
422
+
423
+ detachPolicy(policyId, targetId) {
424
+ const policy = this.organization.detachPolicy(policyId, targetId);
425
+
426
+ this.persistOrganization();
427
+ this.audit.record({
428
+ eventName: 'DetachPolicy',
429
+ readOnly: false,
430
+ resources: [{ ARN: policy.arn, type: 'AWS::Organizations::Policy' }],
431
+ requestParameters: { policyId, targetId }
432
+ });
433
+
434
+ logger.info(`✅ Policy desanexada: ${policyId} -> ${targetId}`);
435
+
436
+ return {};
437
+ }
438
+
439
+ listPoliciesForTarget(targetId, type = 'SERVICE_CONTROL_POLICY') {
440
+ const policies = this.organization.listPoliciesForTarget(targetId, type);
441
+
442
+ return { Policies: policies.map(p => ({ PolicySummary: p })) };
443
+ }
444
+
445
+ /**
446
+ * Avalia se uma ação é permitida por SCPs
447
+ */
448
+ evaluateSCPs(accountId, action, resource = '*') {
449
+ const account = this.organization.getAccount(accountId);
450
+ if (!account) throw new Error(`Account '${accountId}' not found`);
451
+
452
+ const applicablePolicies = [];
453
+
454
+ let currentOUId = account.parentId;
455
+ while (currentOUId) {
456
+ applicablePolicies.push(
457
+ ...this.organization.listPoliciesForTarget(currentOUId, 'SERVICE_CONTROL_POLICY')
458
+ );
459
+ const ou = this.organization.getOrganizationalUnit(currentOUId);
460
+ currentOUId = ou?.parentId;
461
+ }
462
+
463
+ applicablePolicies.push(
464
+ ...this.organization.listPoliciesForTarget(accountId, 'SERVICE_CONTROL_POLICY')
465
+ );
466
+
467
+ for (const policy of applicablePolicies) {
468
+ if (!policy.evaluateAction(action, resource)) {
469
+ return { allowed: false, deniedBy: [policy.name], action, resource };
470
+ }
471
+ }
472
+
473
+ return { allowed: true, action, resource };
474
+ }
475
+
476
+ // ============ Convites (Handshakes) ============
477
+
478
+ inviteAccount(email, config = {}) {
479
+ if (!email) throw new Error('Email is required to invite an account');
480
+
481
+ const invitation = new Invitation({
482
+ organizationId: this.organization.id,
483
+ email,
484
+ accountId: config.accountId || null,
485
+ message: config.message || ''
486
+ });
487
+
488
+ this.invitations.set(invitation.id, invitation);
489
+ this.persistOrganization();
490
+ this.audit.record({
491
+ eventName: 'InviteAccountToOrganization',
492
+ readOnly: false,
493
+ resources: [{ ARN: `arn:aws:organizations::123456789012:handshake/o-${this.organization.id}/invite/${invitation.id}`, type: 'AWS::Organizations::Handshake' }],
494
+ requestParameters: { email }
495
+ });
496
+
497
+ logger.info(`✅ Convite enviado para: ${email}`);
498
+
499
+ return { Handshake: invitation.toHandshake() };
500
+ }
501
+
502
+ listInvitations() {
503
+ return { Handshakes: Array.from(this.invitations.values()).map(i => i.toHandshake()) };
504
+ }
505
+
506
+ describeInvitation(handshakeId) {
507
+ const invitation = this.invitations.get(handshakeId);
508
+ if (!invitation) throw new Error(`Handshake '${handshakeId}' not found`);
509
+
510
+ return { Handshake: invitation.toHandshake() };
511
+ }
512
+
513
+ cancelInvitation(handshakeId) {
514
+ const invitation = this.invitations.get(handshakeId);
515
+ if (!invitation) throw new Error(`Handshake '${handshakeId}' not found`);
516
+ if (invitation.state !== 'REQUESTED') {
517
+ throw new Error(`Handshake '${handshakeId}' is not in REQUESTED state`);
518
+ }
519
+
520
+ invitation.state = 'CANCELED';
521
+ invitation.completedTimestamp = Math.floor(Date.now() / 1000);
522
+ this.invitations.set(invitation.id, invitation);
523
+ this.persistOrganization();
524
+
525
+ logger.info(`✅ Convite cancelado: ${invitation.email}`);
526
+
527
+ return { Handshake: invitation.toHandshake() };
528
+ }
529
+
530
+ // ============ Roles ============
531
+
532
+ createRole(roleName, trustPolicy, config = {}) {
533
+ const role = new OrganizationRole(roleName, trustPolicy, config);
534
+ this.roles.set(role.id, role);
535
+
536
+ this.persistOrganization();
537
+ this.audit.record({
538
+ eventName: 'CreateRole',
539
+ readOnly: false,
540
+ resources: [{ ARN: role.arn, type: 'AWS::IAM::Role' }],
541
+ requestParameters: { roleName }
542
+ });
543
+
544
+ logger.info(`✅ Role criada: ${roleName}`);
545
+
546
+ return { Role: role.toJSON() };
547
+ }
548
+
549
+ getRole(roleId) {
550
+ const role = this.roles.get(roleId);
551
+ if (!role) throw new Error(`Role '${roleId}' not found`);
552
+
553
+ return { Role: role.toJSON() };
554
+ }
555
+
556
+ listRoles() {
557
+ return { Roles: Array.from(this.roles.values()).map(r => r.toJSON()) };
558
+ }
559
+
560
+ putRolePolicy(roleId, policyName, policyDocument) {
561
+ const role = this.roles.get(roleId);
562
+ if (!role) throw new Error(`Role '${roleId}' not found`);
563
+
564
+ role.putInlinePolicy(policyName, policyDocument);
565
+ this.persistOrganization();
566
+
567
+ return {};
568
+ }
569
+
570
+ // ============ Estatísticas / Reset ============
571
+
572
+ getStats() {
573
+ return {
574
+ organizationId: this.organization.id,
575
+ accountCount: this.organization.accountCount,
576
+ ouCount: this.organization.ous.size,
577
+ policyCount: this.organization.policies.size,
578
+ roleCount: this.roles.size,
579
+ invitationCount: this.invitations.size
580
+ };
581
+ }
582
+
583
+ async reset() {
584
+ this.organization = new Organization();
585
+ this.roles.clear();
586
+ this.invitations.clear();
587
+ await this._persist();
588
+
589
+ logger.info('✅ Organizations Simulator resetado');
590
+ }
591
+ }
592
+
593
+ module.exports = OrganizationsSimulator;