@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.
Files changed (150) hide show
  1. package/README.md +662 -658
  2. package/package.json +11 -4
  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/handler-loader.js +50 -5
  100. package/src/services/lambda/index.js +26 -0
  101. package/src/services/lambda/server.js +18 -12
  102. package/src/services/lambda/simulator.js +73 -17
  103. package/src/services/opensearch/index.js +105 -0
  104. package/src/services/opensearch/models/document.js +115 -0
  105. package/src/services/opensearch/models/index.js +73 -0
  106. package/src/services/opensearch/server.js +310 -0
  107. package/src/services/opensearch/simulator.js +605 -0
  108. package/src/services/opensearch/store.js +162 -0
  109. package/src/services/organizations/control-tower-simulator.js +321 -0
  110. package/src/services/organizations/control-tower-store.js +182 -0
  111. package/src/services/organizations/iam-integration.js +413 -0
  112. package/src/services/organizations/index.js +172 -0
  113. package/src/services/organizations/models/index.js +13 -0
  114. package/src/services/organizations/models/invitation.js +48 -0
  115. package/src/services/organizations/models/organization.js +566 -0
  116. package/src/services/organizations/models/role.js +244 -0
  117. package/src/services/organizations/models/scp.js +131 -0
  118. package/src/services/organizations/server.js +736 -0
  119. package/src/services/organizations/simulator.js +593 -0
  120. package/src/services/organizations/store.js +235 -0
  121. package/src/services/organizations/vpc-integration.js +453 -0
  122. package/src/services/route53/index.js +67 -0
  123. package/src/services/route53/models/healthCheck.js +101 -0
  124. package/src/services/route53/models/hostedZone.js +37 -0
  125. package/src/services/route53/models/resourceRecord.js +106 -0
  126. package/src/services/route53/server.js +841 -0
  127. package/src/services/route53/simulator.js +350 -0
  128. package/src/services/s3/index.js +62 -0
  129. package/src/services/s3/presigned-url-generator.js +337 -0
  130. package/src/services/s3/server.js +231 -0
  131. package/src/services/s3/simulator.js +551 -4
  132. package/src/services/sns/index.js +31 -0
  133. package/src/services/sns/server.js +9 -0
  134. package/src/services/sqs/index.js +38 -0
  135. package/src/services/sqs/server.js +57 -4
  136. package/src/services/sqs/simulator.js +423 -33
  137. package/src/services/sts/simulator.js +209 -17
  138. package/src/services/vpc/README.md +459 -0
  139. package/src/services/vpc/ec2ResponseFormatter.js +431 -0
  140. package/src/services/vpc/index.js +90 -0
  141. package/src/services/vpc/models/internetGateway.js +71 -0
  142. package/src/services/vpc/models/routeTable.js +160 -0
  143. package/src/services/vpc/models/subnet.js +48 -0
  144. package/src/services/vpc/models/vpc.js +37 -0
  145. package/src/services/vpc/models/vpcEndpoint.js +100 -0
  146. package/src/services/vpc/models/vpcGateway.js +134 -0
  147. package/src/services/vpc/server.js +836 -0
  148. package/src/services/vpc/simulator.js +613 -0
  149. package/src/utils/policy-cache.js +215 -0
  150. package/aws-config +0 -154
@@ -0,0 +1,736 @@
1
+ /**
2
+ * AWS Organizations & Control Tower Server - REST API
3
+ *
4
+ * Protocolo: AWS JSON 1.1 (mesmo das demais APIs JSON do simulador).
5
+ * POST / com header `X-Amz-Target: AWSOrganizationsV20161128.<Action>`
6
+ * Content-Type: application/x-amz-json-1.1
7
+ *
8
+ * As respostas usam PascalCase (nomes do modelo do AWS), para que o AWS CLI e o
9
+ * AWS SDK interpretem corretamente — o mesmo cuidado aplicado na correção do IAM.
10
+ */
11
+
12
+ const express = require('express');
13
+ const cors = require('cors');
14
+ const crypto = require('crypto');
15
+ const logger = require('../../utils/logger');
16
+
17
+ /** Converte timestamp ISO/epoch em epoch seconds (formato do AWS JSON). */
18
+ function toEpoch(value) {
19
+ if (value === undefined || value === null) return undefined;
20
+ if (typeof value === 'number') return value;
21
+ const date = new Date(value);
22
+ return Number.isNaN(date.getTime()) ? undefined : Math.floor(date.getTime() / 1000);
23
+ }
24
+
25
+ /** Remove chaves undefined para manter a resposta limpa. */
26
+ function compact(obj) {
27
+ const out = {};
28
+ for (const [key, value] of Object.entries(obj)) {
29
+ if (value !== undefined) out[key] = value;
30
+ }
31
+ return out;
32
+ }
33
+
34
+ const serializers = {
35
+ organization(org) {
36
+ return compact({
37
+ Id: org.Id,
38
+ Arn: org.Arn,
39
+ FeatureSet: org.FeatureSet,
40
+ MasterAccountArn: org.MasterAccountArn,
41
+ MasterAccountId: org.MasterAccountId,
42
+ MasterAccountEmail: org.MasterAccountEmail,
43
+ AvailablePolicyTypes: [{ Type: 'SERVICE_CONTROL_POLICY', Status: 'ENABLED' }]
44
+ });
45
+ },
46
+
47
+ root(root) {
48
+ return compact({ Id: root.Id, Arn: root.Arn, Name: root.Name, PolicyTypes: root.PolicyTypes });
49
+ },
50
+
51
+ account(account) {
52
+ return compact({
53
+ Id: account.id,
54
+ Arn: account.arn,
55
+ Email: account.email,
56
+ Name: account.name,
57
+ Status: account.status,
58
+ State: account.status === 'ACTIVE' ? 'ACTIVE' : account.status,
59
+ JoinedMethod: account.joinedMethod,
60
+ JoinedTimestamp: toEpoch(account.createdAt)
61
+ });
62
+ },
63
+
64
+ organizationalUnit(ou) {
65
+ return compact({ Id: ou.id, Arn: ou.arn, Name: ou.name });
66
+ },
67
+
68
+ createAccountStatus(status) {
69
+ return compact({
70
+ Id: status.Id,
71
+ AccountName: status.AccountName,
72
+ State: status.State,
73
+ RequestedTimestamp: toEpoch(status.RequestedTimestamp),
74
+ CompletedTimestamp: toEpoch(status.CompletedTimestamp),
75
+ AccountId: status.AccountId,
76
+ FailureReason: status.FailureReason
77
+ });
78
+ },
79
+
80
+ policySummary(policy) {
81
+ return compact({
82
+ Id: policy.id,
83
+ Arn: policy.arn,
84
+ Name: policy.name,
85
+ Type: policy.type,
86
+ AwsManaged: false,
87
+ Description: policy.description || ''
88
+ });
89
+ },
90
+
91
+ serviceAccess(entry) {
92
+ return compact({
93
+ ServicePrincipal: entry.ServicePrincipal,
94
+ DateEnabled: toEpoch(entry.DateEnabled)
95
+ });
96
+ }
97
+ };
98
+
99
+ class OrganizationsServer {
100
+ constructor(port, config) {
101
+ this.port = port;
102
+ this.config = config;
103
+ this.app = express();
104
+ this.organizationsSimulator = null;
105
+ this.controlTowerSimulator = null;
106
+ this.server = null;
107
+ this.setupMiddlewares();
108
+ }
109
+
110
+ setupMiddlewares() {
111
+ this.app.use(cors());
112
+ // O AWS CLI/SDK envia application/x-amz-json-1.1; sem esta lista o body não
113
+ // seria parseado e as ações com parâmetros falhariam.
114
+ this.app.use(express.json({
115
+ limit: '10mb',
116
+ type: ['application/json', 'application/x-amz-json-1.1']
117
+ }));
118
+
119
+ this.app.use((req, res, next) => {
120
+ const target = req.headers['x-amz-target'];
121
+ if (target) {
122
+ const [, action] = target.split('.');
123
+ req.action = action;
124
+ }
125
+
126
+ logger.debug(`Organizations: ${req.method} ${req.path} - Action: ${req.action || 'none'}`);
127
+ next();
128
+ });
129
+ }
130
+
131
+ async initialize() {
132
+ logger.debug(`Inicializando Organizations Server na porta ${this.port}`);
133
+ this.setupRoutes();
134
+ }
135
+
136
+ setupRoutes() {
137
+ // ========== API AWS (AWS JSON 1.1) ==========
138
+ this.app.post('/', (req, res) => {
139
+ const action = req.action || req.body?.Action;
140
+
141
+ if (!action) {
142
+ return this._jsonError(res, 'MissingAction', 'The request must contain an Action', 400);
143
+ }
144
+
145
+ try {
146
+ const result = this._handleAction(action, req.body || {});
147
+ this._jsonResponse(res, result);
148
+ } catch (error) {
149
+ this._handleError(res, error, action);
150
+ }
151
+ });
152
+
153
+ // ========== API deAdmin (dashboard) ==========
154
+ this.setupAdminRoutes();
155
+
156
+ // Health check
157
+ this.app.get('/health', (req, res) => {
158
+ res.json({
159
+ status: 'healthy',
160
+ service: 'organizations',
161
+ port: this.port,
162
+ stats: {
163
+ organizations: this.organizationsSimulator?.getStats(),
164
+ controlTower: this.controlTowerSimulator?.getStats()
165
+ }
166
+ });
167
+ });
168
+ }
169
+
170
+ setupAdminRoutes() {
171
+ this.app.get('/__admin/health', (req, res) => {
172
+ res.json({ status: 'ok', service: 'organizations' });
173
+ });
174
+
175
+ // Organização (detalhes + estatísticas) — tela de Settings
176
+ this.app.get('/__admin/organization', (req, res) => {
177
+ try {
178
+ const { Organization: org } = this.organizationsSimulator.describeOrganization();
179
+ const accounts = this.organizationsSimulator.listAccounts().Accounts;
180
+ res.json({
181
+ organization: serializers.organization(org),
182
+ stats: this.organizationsSimulator.getStats(),
183
+ managementAccount: {
184
+ Id: accounts.find(a => a.type === 'MASTER')?.id || org.MasterAccountId,
185
+ Name: accounts.find(a => a.type === 'MASTER')?.name || org.MasterAccountName,
186
+ Email: accounts.find(a => a.type === 'MASTER')?.email || org.MasterAccountEmail,
187
+ Arn: org.MasterAccountArn
188
+ }
189
+ });
190
+ } catch (err) {
191
+ this._adminError(res, err);
192
+ }
193
+ });
194
+
195
+ this.app.get('/__admin/accounts', (req, res) => {
196
+ try {
197
+ const accounts = this.organizationsSimulator.listAccounts().Accounts;
198
+ res.json(accounts.map((account) => ({ ...serializers.account(account), ParentId: account.parentId })));
199
+ } catch (err) {
200
+ this._adminError(res, err);
201
+ }
202
+ });
203
+
204
+ this.app.post('/__admin/accounts/create', (req, res) => {
205
+ const { AccountName, Email, RoleName, ParentId, Tags } = req.body || {};
206
+ if (!Email) return res.status(400).json({ message: 'Email is required' });
207
+ if (!AccountName) return res.status(400).json({ message: 'AccountName is required' });
208
+
209
+ try {
210
+ const result = this.organizationsSimulator.createAccount(Email, AccountName, {
211
+ parentOUId: ParentId,
212
+ roleName: RoleName,
213
+ tags: Tags
214
+ });
215
+ const status = serializers.createAccountStatus(result.CreateAccountStatus);
216
+ const account = this.organizationsSimulator
217
+ .describeAccount(status.AccountId).Account;
218
+ res.status(201).json({
219
+ ...status,
220
+ Account: { ...serializers.account(account), ParentId: account.parentId }
221
+ });
222
+ } catch (err) {
223
+ this._adminError(res, err);
224
+ }
225
+ });
226
+
227
+ this.app.delete('/__admin/accounts/:id', (req, res) => {
228
+ try {
229
+ this.organizationsSimulator.removeAccountFromOrganization(req.params.id);
230
+ res.json({ success: true });
231
+ } catch (err) {
232
+ this._adminError(res, err);
233
+ }
234
+ });
235
+
236
+ this.app.post('/__admin/accounts/move', (req, res) => {
237
+ const { AccountId, SourceParentId, DestinationParentId } = req.body || {};
238
+ if (!AccountId) return res.status(400).json({ message: 'AccountId is required' });
239
+ if (!DestinationParentId) return res.status(400).json({ message: 'DestinationParentId is required' });
240
+
241
+ try {
242
+ this.organizationsSimulator.moveAccount(AccountId, SourceParentId, DestinationParentId);
243
+ res.json({ success: true });
244
+ } catch (err) {
245
+ this._adminError(res, err);
246
+ }
247
+ });
248
+
249
+ // OUs — supporting da árvore de contas
250
+ this.app.get('/__admin/organizational-units', (req, res) => {
251
+ try {
252
+ const ous = Array.from(this.organizationsSimulator.organization.ous.values());
253
+ res.json(ous.map(serializerWithChildren));
254
+ } catch (err) {
255
+ this._adminError(res, err);
256
+ }
257
+ });
258
+
259
+ this.app.post('/__admin/organizational-units/create', (req, res) => {
260
+ const { ParentId, Name } = req.body || {};
261
+ if (!Name) return res.status(400).json({ message: 'Name is required' });
262
+ if (!ParentId) return res.status(400).json({ message: 'ParentId is required' });
263
+
264
+ try {
265
+ const { OrganizationalUnit } = this.organizationsSimulator.createOrganizationalUnit(ParentId, Name);
266
+ res.status(201).json(serializerWithChildren(OrganizationalUnit));
267
+ } catch (err) {
268
+ this._adminError(res, err);
269
+ }
270
+ });
271
+
272
+ this.app.delete('/__admin/organizational-units/:id', (req, res) => {
273
+ try {
274
+ this.organizationsSimulator.deleteOrganizationalUnit(req.params.id);
275
+ res.json({ success: true });
276
+ } catch (err) {
277
+ this._adminError(res, err);
278
+ }
279
+ });
280
+
281
+ // Convites — tela de Invitations
282
+ this.app.get('/__admin/invitations', (req, res) => {
283
+ try {
284
+ res.json(this.organizationsSimulator.listInvitations().Handshakes);
285
+ } catch (err) {
286
+ this._adminError(res, err);
287
+ }
288
+ });
289
+
290
+ this.app.post('/__admin/invitations', (req, res) => {
291
+ const { Email, AccountId, Message } = req.body || {};
292
+ if (!Email) return res.status(400).json({ message: 'Email is required' });
293
+
294
+ try {
295
+ const { Handshake } = this.organizationsSimulator.inviteAccount(Email, {
296
+ accountId: AccountId,
297
+ message: Message
298
+ });
299
+ res.status(201).json(Handshake);
300
+ } catch (err) {
301
+ this._adminError(res, err);
302
+ }
303
+ });
304
+
305
+ this.app.delete('/__admin/invitations/:id', (req, res) => {
306
+ try {
307
+ const { Handshake } = this.organizationsSimulator.cancelInvitation(req.params.id);
308
+ res.json(Handshake);
309
+ } catch (err) {
310
+ this._adminError(res, err);
311
+ }
312
+ });
313
+
314
+ // Policies — leitura para a tela de Policies
315
+ this.app.get('/__admin/policies', (req, res) => {
316
+ try {
317
+ const { Policies } = this.organizationsSimulator.listPolicies();
318
+ res.json(Policies.map(p => serializers.policySummary(p.PolicySummary)));
319
+ } catch (err) {
320
+ this._adminError(res, err);
321
+ }
322
+ });
323
+
324
+ // Policy types — status (Enabled/Disabled) exibido na tela de Policies
325
+ this.app.get('/__admin/policy-types', (req, res) => {
326
+ try {
327
+ res.json(this.organizationsSimulator.listPolicyTypes().PolicyTypes);
328
+ } catch (err) {
329
+ this._adminError(res, err);
330
+ }
331
+ });
332
+
333
+ this.app.post('/__admin/policy-types/enable', (req, res) => {
334
+ const { PolicyType } = req.body || {};
335
+ if (!PolicyType) return res.status(400).json({ message: 'PolicyType is required' });
336
+
337
+ try {
338
+ res.json(this.organizationsSimulator.enablePolicyType(PolicyType));
339
+ } catch (err) {
340
+ this._adminError(res, err);
341
+ }
342
+ });
343
+
344
+ this.app.post('/__admin/policy-types/disable', (req, res) => {
345
+ const { PolicyType } = req.body || {};
346
+ if (!PolicyType) return res.status(400).json({ message: 'PolicyType is required' });
347
+
348
+ try {
349
+ res.json(this.organizationsSimulator.disablePolicyType(PolicyType));
350
+ } catch (err) {
351
+ this._adminError(res, err);
352
+ }
353
+ });
354
+
355
+ // Serviços integrados (trusted access) — tela de Services
356
+ this.app.get('/__admin/service-access', (req, res) => {
357
+ try {
358
+ // ISO para o dashboard formatar no fuso local.
359
+ res.json(this.organizationsSimulator.organization.listServiceAccess());
360
+ } catch (err) {
361
+ this._adminError(res, err);
362
+ }
363
+ });
364
+
365
+ this.app.post('/__admin/service-access/enable', (req, res) => {
366
+ const { ServicePrincipal } = req.body || {};
367
+ if (!ServicePrincipal) return res.status(400).json({ message: 'ServicePrincipal is required' });
368
+
369
+ try {
370
+ res.json(this.organizationsSimulator.enableServiceAccess(ServicePrincipal));
371
+ } catch (err) {
372
+ this._adminError(res, err);
373
+ }
374
+ });
375
+
376
+ this.app.post('/__admin/service-access/disable', (req, res) => {
377
+ const { ServicePrincipal } = req.body || {};
378
+ if (!ServicePrincipal) return res.status(400).json({ message: 'ServicePrincipal is required' });
379
+
380
+ try {
381
+ res.json(this.organizationsSimulator.disableServiceAccess(ServicePrincipal));
382
+ } catch (err) {
383
+ this._adminError(res, err);
384
+ }
385
+ });
386
+ }
387
+
388
+ // ========== Ações AWS ==========
389
+
390
+ /** Valida a existência de uma conta, com o exception type do AWS. */
391
+ _requireAccount(accountId) {
392
+ const account = this.organizationsSimulator.organization.getAccount(accountId);
393
+ if (!account) {
394
+ throw this._awsError('AccountNotFoundException', `Account '${accountId}' not found`, 400);
395
+ }
396
+ return account;
397
+ }
398
+
399
+ /** Valida a existência de uma OU, com o exception type do AWS. */
400
+ _requireOu(ouId) {
401
+ const ou = this.organizationsSimulator.organization.getOrganizationalUnit(ouId);
402
+ if (!ou) {
403
+ throw this._awsError('OrganizationalUnitNotFoundException', `Organizational unit '${ouId}' not found`, 400);
404
+ }
405
+ return ou;
406
+ }
407
+
408
+ /** Valida a existência de uma policy. */
409
+ _requirePolicy(policyId) {
410
+ const policy = this.organizationsSimulator.organization.getPolicy(policyId);
411
+ if (!policy) {
412
+ throw this._awsError('PolicyNotFoundException', `Policy '${policyId}' not found`, 400);
413
+ }
414
+ return { Policy: { PolicySummary: policy, Content: JSON.stringify(policy.document) } };
415
+ }
416
+
417
+ /** Valida a existência de um convite. */
418
+ _requireInvitation(handshakeId) {
419
+ const invitation = this.organizationsSimulator.invitations.get(handshakeId);
420
+ if (!invitation) {
421
+ throw this._awsError('HandshakeNotFoundException', `Handshake '${handshakeId}' not found`, 400);
422
+ }
423
+ return invitation;
424
+ }
425
+
426
+ _handleAction(action, body) {
427
+ const sim = this.organizationsSimulator;
428
+
429
+ switch (action) {
430
+ // Organização / Roots
431
+ case 'DescribeOrganization':
432
+ return {
433
+ Organization: serializers.organization(sim.describeOrganization().Organization)
434
+ };
435
+
436
+ case 'ListRoots':
437
+ return { Roots: sim.listRoots().Roots.map(serializers.root) };
438
+
439
+ // Organizational Units
440
+ case 'CreateOrganizationalUnit':
441
+ if (!body.Name) {
442
+ throw this._awsError('InvalidInputException', 'You must specify a name for the organizational unit', 400);
443
+ }
444
+ this._requireOu(body.ParentId);
445
+ return {
446
+ OrganizationalUnit: serializers.organizationalUnit(
447
+ sim.createOrganizationalUnit(body.ParentId, body.Name).OrganizationalUnit
448
+ )
449
+ };
450
+
451
+ case 'DescribeOrganizationalUnit':
452
+ return {
453
+ OrganizationalUnit: serializers.organizationalUnit(this._requireOu(body.OrganizationalUnitId))
454
+ };
455
+
456
+ case 'ListOrganizationalUnitsForParent':
457
+ this._requireOu(body.ParentId);
458
+ return {
459
+ OrganizationalUnits: sim
460
+ .listOrganizationalUnitsForParent(body.ParentId)
461
+ .OrganizationalUnits.map(serializers.organizationalUnit)
462
+ };
463
+
464
+ case 'DeleteOrganizationalUnit':
465
+ this._requireOu(body.OrganizationalUnitId);
466
+ try {
467
+ return sim.deleteOrganizationalUnit(body.OrganizationalUnitId);
468
+ } catch (err) {
469
+ throw this._awsError('OrganizationalUnitNotEmptyException', err.message, 400);
470
+ }
471
+
472
+ // Contas
473
+ case 'CreateAccount':
474
+ if (!body.Email) {
475
+ throw this._awsError('InvalidInputException', 'You must specify an email for the account', 400);
476
+ }
477
+ if (!body.AccountName) {
478
+ throw this._awsError('InvalidInputException', 'You must specify an account name', 400);
479
+ }
480
+ try {
481
+ return {
482
+ CreateAccountStatus: serializers.createAccountStatus(
483
+ sim.createAccount(body.Email, body.AccountName, { roleName: body.RoleName }).CreateAccountStatus
484
+ )
485
+ };
486
+ } catch (err) {
487
+ throw this._awsError('AccountAlreadyExistsException', err.message, 400);
488
+ }
489
+
490
+ case 'ListAccounts':
491
+ return { Accounts: sim.listAccounts().Accounts.map(serializers.account) };
492
+
493
+ case 'ListAccountsForParent':
494
+ this._requireOu(body.ParentId);
495
+ return {
496
+ Accounts: sim.listAccountsForParent(body.ParentId).Accounts.map(serializers.account)
497
+ };
498
+
499
+ case 'DescribeAccount':
500
+ return { Account: serializers.account(this._requireAccount(body.AccountId)) };
501
+
502
+ case 'MoveAccount':
503
+ this._requireAccount(body.AccountId);
504
+ this._requireOu(body.SourceParentId);
505
+ this._requireOu(body.DestinationParentId);
506
+ return sim.moveAccount(body.AccountId, body.SourceParentId, body.DestinationParentId);
507
+
508
+ case 'RemoveAccountFromOrganization':
509
+ this._requireAccount(body.AccountId);
510
+ return sim.removeAccountFromOrganization(body.AccountId);
511
+
512
+ // Policies
513
+ case 'CreatePolicy': {
514
+ const document = typeof body.Content === 'string' ? JSON.parse(body.Content) : body.Content;
515
+ const { Policy } = sim.createPolicy(body.Name, document, body.Type);
516
+ return {
517
+ Policy: {
518
+ PolicySummary: serializers.policySummary(Policy.PolicySummary),
519
+ Content: Policy.Content
520
+ }
521
+ };
522
+ }
523
+
524
+ case 'DescribePolicy': {
525
+ const { Policy } = this._requirePolicy(body.PolicyId);
526
+ return {
527
+ Policy: {
528
+ PolicySummary: serializers.policySummary(Policy.PolicySummary),
529
+ Content: Policy.Content
530
+ }
531
+ };
532
+ }
533
+
534
+ case 'ListPolicies':
535
+ return {
536
+ Policies: sim.listPolicies(body.Filter).Policies.map(p => ({
537
+ PolicySummary: serializers.policySummary(p.PolicySummary)
538
+ }))
539
+ };
540
+
541
+ case 'DeletePolicy':
542
+ this._requirePolicy(body.PolicyId);
543
+ return sim.deletePolicy(body.PolicyId);
544
+
545
+ case 'AttachPolicy':
546
+ this._requirePolicy(body.PolicyId);
547
+ return sim.attachPolicy(body.PolicyId, body.TargetId);
548
+
549
+ case 'DetachPolicy':
550
+ this._requirePolicy(body.PolicyId);
551
+ return sim.detachPolicy(body.PolicyId, body.TargetId);
552
+
553
+ case 'ListPoliciesForTarget':
554
+ return {
555
+ Policies: sim
556
+ .listPoliciesForTarget(body.TargetId, body.Filter)
557
+ .Policies.map(p => ({ PolicySummary: serializers.policySummary(p.PolicySummary) }))
558
+ };
559
+
560
+ case 'EnablePolicyType': {
561
+ if (!body.PolicyType) {
562
+ throw this._awsError('InvalidInputException', 'You must specify a policy type', 400);
563
+ }
564
+ sim.enablePolicyType(body.PolicyType);
565
+ return { Root: serializers.root(sim.getRoot()) };
566
+ }
567
+
568
+ case 'DisablePolicyType': {
569
+ if (!body.PolicyType) {
570
+ throw this._awsError('InvalidInputException', 'You must specify a policy type', 400);
571
+ }
572
+ sim.disablePolicyType(body.PolicyType);
573
+ return { Root: serializers.root(sim.getRoot()) };
574
+ }
575
+
576
+ // Serviços integrados (trusted access)
577
+ case 'EnableAWSServiceAccess': {
578
+ if (!body.ServicePrincipal) {
579
+ throw this._awsError('InvalidInputException', 'You must specify a service principal', 400);
580
+ }
581
+ sim.enableServiceAccess(body.ServicePrincipal);
582
+ return {};
583
+ }
584
+
585
+ case 'DisableAWSServiceAccess': {
586
+ if (!body.ServicePrincipal) {
587
+ throw this._awsError('InvalidInputException', 'You must specify a service principal', 400);
588
+ }
589
+ sim.disableServiceAccess(body.ServicePrincipal);
590
+ return {};
591
+ }
592
+
593
+ case 'ListAWSServiceAccessForOrganization':
594
+ return {
595
+ EnabledServicePrincipals: sim
596
+ .listServiceAccess()
597
+ .EnabledServicePrincipals.map(serializers.serviceAccess)
598
+ };
599
+
600
+ // Convites
601
+ case 'InviteAccountToOrganization': {
602
+ const target = body.Target?.Id || body.Email;
603
+ if (!target) {
604
+ throw this._awsError('InvalidInputException', 'You must specify a target to invite', 400);
605
+ }
606
+ return {
607
+ Handshake: sim.inviteAccount(target, {
608
+ accountId: body.Target?.Type === 'ACCOUNT' ? body.Target?.Id : undefined,
609
+ message: body.Note
610
+ }).Handshake
611
+ };
612
+ }
613
+
614
+ case 'ListHandshakesForOrganization':
615
+ return { Handshakes: sim.listInvitations().Handshakes };
616
+
617
+ case 'DescribeHandshake':
618
+ return { Handshake: this._requireInvitation(body.HandshakeId).toHandshake() };
619
+
620
+ case 'CancelHandshake': {
621
+ const invitation = this._requireInvitation(body.HandshakeId);
622
+ if (invitation.state !== 'REQUESTED') {
623
+ throw this._awsError(
624
+ 'HandshakeConstraintViolationException',
625
+ `Handshake '${invitation.id}' is not in REQUESTED state`,
626
+ 400
627
+ );
628
+ }
629
+ return { Handshake: sim.cancelInvitation(invitation.id).Handshake };
630
+ }
631
+
632
+ // Avaliação de SCP (uso interno/simulação)
633
+ case 'EvaluateSCPs':
634
+ return sim.evaluateSCPs(body.AccountId, body.Action, body.Resource);
635
+
636
+ // Control Tower
637
+ case 'SetupLandingZone':
638
+ return this.controlTowerSimulator.setupLandingZone(body.OrganizationRootId);
639
+ case 'GetLandingZoneStatus':
640
+ return this.controlTowerSimulator.getLandingZoneStatus(body.LandingZoneId);
641
+ case 'ListGuardrails':
642
+ return this.controlTowerSimulator.listGuardrails();
643
+ case 'EnableGuardrail':
644
+ return this.controlTowerSimulator.enableGuardrail(body.GuardrailId, body.TargetOUId);
645
+ case 'DisableGuardrail':
646
+ return this.controlTowerSimulator.disableGuardrail(body.GuardrailId, body.TargetOUId);
647
+ case 'CheckCompliance':
648
+ return this.controlTowerSimulator.checkCompliance(body.AccountId);
649
+ case 'GenerateComplianceReport':
650
+ return this.controlTowerSimulator.generateComplianceReport();
651
+
652
+ default:
653
+ throw this._awsError('UnknownActionException', `Unknown action: ${action}`, 400);
654
+ }
655
+ }
656
+
657
+ _awsError(code, message, statusCode = 400) {
658
+ const err = new Error(message);
659
+ err.code = code;
660
+ err.statusCode = statusCode;
661
+ return err;
662
+ }
663
+
664
+ _handleError(res, err, action) {
665
+ const code = err.code || 'InternalFailureException';
666
+ const statusCode = err.statusCode || 500;
667
+ logger.error(`Organizations Error [${code}] em ${action}: ${err.message}`);
668
+ this._jsonError(res, code, err.message, statusCode);
669
+ }
670
+
671
+ _adminError(res, err) {
672
+ const statusCode = err.statusCode || 400;
673
+ logger.error(`Organizations Admin Error: ${err.message}`);
674
+ res.status(statusCode).json({ message: err.message });
675
+ }
676
+
677
+ _jsonResponse(res, result) {
678
+ res.set('Content-Type', 'application/x-amz-json-1.1');
679
+ res.send(JSON.stringify(result || {}));
680
+ }
681
+
682
+ _jsonError(res, code, message, statusCode = 400) {
683
+ logger.debug(`Organizations erro ${code}: ${message}`);
684
+ res.status(statusCode)
685
+ .set('Content-Type', 'application/x-amz-json-1.1')
686
+ .send(JSON.stringify({
687
+ __type: code,
688
+ message
689
+ }));
690
+ }
691
+
692
+ async start() {
693
+ return new Promise((resolve, reject) => {
694
+ try {
695
+ this.server = this.app.listen(this.port, () => {
696
+ logger.info(`✅ Organizations Server iniciado na porta ${this.port}`);
697
+ resolve();
698
+ });
699
+ } catch (error) {
700
+ logger.error(`❌ Erro ao iniciar Organizations Server: ${error.message}`);
701
+ reject(error);
702
+ }
703
+ });
704
+ }
705
+
706
+ async stop() {
707
+ return new Promise((resolve, reject) => {
708
+ if (this.server) {
709
+ this.server.close((err) => {
710
+ if (err) {
711
+ logger.error(`Erro ao parar Organizations Server: ${err.message}`);
712
+ reject(err);
713
+ } else {
714
+ logger.info('Organizations Server parado');
715
+ resolve();
716
+ }
717
+ });
718
+ } else {
719
+ resolve();
720
+ }
721
+ });
722
+ }
723
+ }
724
+
725
+ /** Serializer de OU para a API de admin (inclui hierarquia). */
726
+ function serializerWithChildren(ou) {
727
+ return {
728
+ Id: ou.id,
729
+ Arn: ou.arn,
730
+ Name: ou.name,
731
+ ParentId: ou.parentId,
732
+ Children: ou.children || []
733
+ };
734
+ }
735
+
736
+ module.exports = OrganizationsServer;