@izara_project/izara-core-generate-service-code 1.0.54 → 1.0.56

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 (52) hide show
  1. package/package.json +1 -1
  2. package/src/codeGenerators/app/initial_setup/InitialSetupGenerator.js +223 -123
  3. package/src/codeGenerators/app/initial_setup/templates/InitialSetup_LambdaRole.ejs +6 -6
  4. package/src/codeGenerators/app/sls_yaml/FindDataYamlGenerator.js +1 -1
  5. package/src/codeGenerators/app/sls_yaml/FunctionYamlGenerator.js +303 -201
  6. package/src/codeGenerators/app/sls_yaml/RoleNameConfigGenerator.js +84 -60
  7. package/src/codeGenerators/app/sls_yaml/SharedResourceYamlGenerator.js +304 -271
  8. package/src/codeGenerators/app/sls_yaml/__tests__/SharedResourceYamlGenerator.test.js +91 -32
  9. package/src/codeGenerators/app/sls_yaml/templates/SharedResource_Yaml.ejs +2 -2
  10. package/src/codeGenerators/app/src/generatedCode/Flow/{FlowEndpoints → FlowObjects}/EndpointsGenerator.js +1 -1
  11. package/src/codeGenerators/app/src/generatedCode/Flow/FlowRbac/templates/rbac/FlowRbac_Main.ejs +252 -0
  12. package/src/codeGenerators/app/src/generatedCode/Flow/{FlowRelationshipEndpoints → FlowRelationships}/RelationshipFlowGenerator.js +1 -1
  13. package/src/codeGenerators/app/src/generatedCode/Flow/FlowSchemas/StatusFieldGenerator.js +1 -8
  14. package/src/codeGenerators/app/src/generatedCode/Flow/FlowSchemas/WebSocketGenerator.js +1 -7
  15. package/src/codeGenerators/app/src/generatedCode/Flow/_shared/events/BaseSqsHandler.js +3 -3
  16. package/src/codeGenerators/app/src/generatedCode/Flow/_shared/shared/flowClassifier.js +3 -2
  17. package/src/codeGenerators/app/src/generatedCode/Flow/_shared/shared/flowMainFunctionBase.js +12 -5
  18. package/src/codeGenerators/app/src/generatedCode/Flow/_shared/shared/triggerCacheBase.js +1 -7
  19. package/src/codeGenerators/app/src/generatedCode/SystemFlowSchemas/RegisterGenerator.js +1 -7
  20. package/src/codeGenerators/app/src/generatedCode/libs/templates/Consts.ejs +23 -23
  21. package/src/codeGenerators/resource/sls_yaml/DynamoDBGenerator.js +8 -8
  22. package/src/codeGenerators/resource/sls_yaml/FlowOutGenerator.js +57 -51
  23. package/src/codeGenerators/resource/sls_yaml/FlowResourceYamlGenerator.js +164 -175
  24. package/src/codeGenerators/resource/sls_yaml/templates/SystemDynamoDB_Yaml.ejs +64 -0
  25. package/src/codeGenerators/resource/sls_yaml/templates/crud/ResourceYaml.ejs +10 -10
  26. package/src/generate.js +1 -1
  27. package/src/generateCode.js +181 -149
  28. package/src/generateSchema.js +1 -1
  29. package/src/schemaGenerators/app/src/schemas/FlowSchemas/FlowSchemaGenerator.js +0 -1
  30. package/src/schemaGenerators/app/src/schemas/FlowSchemas/RbacFlowSchemaGenerator.js +16 -2
  31. package/src/schemaGenerators/app/src/schemas/FlowSchemas/templates/DynamicFlowSchemaTemplate.ejs +3 -1
  32. package/src/schemaGenerators/app/src/schemas/FlowSchemas/templates/DynamicRbacFlowSchemaTemplate.ejs +1 -0
  33. package/src/schemaGenerators/app/src/schemas/FlowSchemas/templates/RelationshipFlowSchemaTemplate.ejs +6 -5
  34. package/src/schemaGenerators/app/src/schemas/FlowSchemas/templates/UserRbacFlowSchemaTemplate.ejs +22 -0
  35. /package/src/codeGenerators/app/src/generatedCode/Flow/{FlowEndpoints → FlowObjects}/.gitkeep +0 -0
  36. /package/src/codeGenerators/app/src/generatedCode/Flow/{FlowEndpoints → FlowObjects}/templates/FlowEndpointBeforeLogical_Main.ejs +0 -0
  37. /package/src/codeGenerators/app/src/generatedCode/Flow/{FlowEndpoints → FlowObjects}/templates/crud/CreateEndpoint_Main.ejs +0 -0
  38. /package/src/codeGenerators/app/src/generatedCode/Flow/{FlowEndpoints → FlowObjects}/templates/crud/DeleteEndpoint_Main.ejs +0 -0
  39. /package/src/codeGenerators/app/src/generatedCode/Flow/{FlowEndpoints → FlowObjects}/templates/crud/GetEndpoint_Main.ejs +0 -0
  40. /package/src/codeGenerators/app/src/generatedCode/Flow/{FlowEndpoints → FlowObjects}/templates/crud/UpdateEndpoint_Main.ejs +0 -0
  41. /package/src/codeGenerators/app/src/generatedCode/Flow/{FlowRelationshipEndpoints → FlowRelationships}/templates/relationship/ProcessChangeRelationshipComplete_Main.ejs +0 -0
  42. /package/src/codeGenerators/app/src/generatedCode/Flow/{FlowRelationshipEndpoints → FlowRelationships}/templates/relationship/ProcessChangeRelationship_Main.ejs +0 -0
  43. /package/src/codeGenerators/app/src/generatedCode/Flow/{FlowRelationshipEndpoints → FlowRelationships}/templates/relationship/ProcessCreateRelationshipComplete_Main.ejs +0 -0
  44. /package/src/codeGenerators/app/src/generatedCode/Flow/{FlowRelationshipEndpoints → FlowRelationships}/templates/relationship/ProcessCreateRelationship_Main.ejs +0 -0
  45. /package/src/codeGenerators/app/src/generatedCode/Flow/{FlowRelationshipEndpoints → FlowRelationships}/templates/relationship/ProcessDeleteRelationshipComplete_Main.ejs +0 -0
  46. /package/src/codeGenerators/app/src/generatedCode/Flow/{FlowRelationshipEndpoints → FlowRelationships}/templates/relationship/ProcessDeleteRelationship_Main.ejs +0 -0
  47. /package/src/codeGenerators/app/src/generatedCode/Flow/{FlowRelationshipEndpoints → FlowRelationships}/templates/relationship/ProcessGetRelationshipComplete_Main.ejs +0 -0
  48. /package/src/codeGenerators/app/src/generatedCode/Flow/{FlowRelationshipEndpoints → FlowRelationships}/templates/relationship/ProcessGetRelationship_Main.ejs +0 -0
  49. /package/src/codeGenerators/app/src/generatedCode/Flow/{FlowRelationshipEndpoints → FlowRelationships}/templates/relationship/ProcessMoveRelationshipComplete_Main.ejs +0 -0
  50. /package/src/codeGenerators/app/src/generatedCode/Flow/{FlowRelationshipEndpoints → FlowRelationships}/templates/relationship/ProcessMoveRelationship_Main.ejs +0 -0
  51. /package/src/codeGenerators/app/src/generatedCode/Flow/{FlowRelationshipEndpoints → FlowRelationships}/templates/relationship/ProcessUpdateRelationshipComplete_Main.ejs +0 -0
  52. /package/src/codeGenerators/app/src/generatedCode/Flow/{FlowRelationshipEndpoints → FlowRelationships}/templates/relationship/ProcessUpdateRelationship_Main.ejs +0 -0
@@ -11,14 +11,29 @@ const __dirname = path.dirname(__filename);
11
11
  const APP_SOURCE_FILES = [
12
12
  'find-data.yml',
13
13
  'flow-schema.yml',
14
- 'per-action-function.yml',
14
+ 'flow-rbac.yml',
15
+ 'flow-objects.yml',
15
16
  'process-logical.yml',
16
- 'relationship-per-action.yml'
17
+ 'flow-relationships.yml'
17
18
  ];
18
19
 
20
+ const FLOW_OBJECTS_ROLE = 'FlowObjectsRole';
21
+
19
22
  const QUEUE_OWNERSHIP_PREFIXES = {
20
- PerActionEndpointRole: ['Create', 'Update', 'Delete', 'Get'],
21
- RelationshipRole: [
23
+ [FLOW_OBJECTS_ROLE]: ['Create', 'Update', 'Delete', 'Get'],
24
+ FlowRbacRole: [
25
+ 'CreateTargetRole',
26
+ 'ListTargetRole',
27
+ 'DeleteTargetRole',
28
+ 'CreateRolePermissions',
29
+ 'ListRolePermissions',
30
+ 'DeleteRolePermissions',
31
+ 'CreateUserRole',
32
+ 'ListUserInRoles',
33
+ 'DeleteUserFromRole',
34
+ 'UserRbacFlow'
35
+ ],
36
+ FlowRelationshipRole: [
22
37
  'CreateRelationship',
23
38
  'UpdateRelationship',
24
39
  'DeleteRelationship',
@@ -31,6 +46,54 @@ const QUEUE_OWNERSHIP_PREFIXES = {
31
46
  WebSocketMainRole: ['WebSocket']
32
47
  };
33
48
 
49
+ const COMMON_S3_STATEMENTS = [
50
+ {
51
+ action: 's3:GetObject',
52
+ resource:
53
+ 'arn:aws:s3:::${self:custom.iz_serviceSchemaBucketName}/perServiceSchemas/*'
54
+ },
55
+ {
56
+ action: 's3:GetObjectVersion',
57
+ resource:
58
+ 'arn:aws:s3:::${self:custom.iz_serviceSchemaBucketName}/perServiceSchemas/*'
59
+ },
60
+ {
61
+ action: 's3:GetObject',
62
+ resource:
63
+ 'arn:aws:s3:::${self:custom.iz_serviceSchemaBucketName}/serviceConfig/GraphServerTags.json'
64
+ },
65
+ {
66
+ action: 's3:GetObjectVersion',
67
+ resource:
68
+ 'arn:aws:s3:::${self:custom.iz_serviceSchemaBucketName}/serviceConfig/GraphServerTags.json'
69
+ },
70
+ {
71
+ action: 's3:ListBucket',
72
+ resource: 'arn:aws:s3:::${self:custom.iz_serviceSchemaBucketName}'
73
+ }
74
+ ];
75
+
76
+ const DEFAULT_DYNAMO_ACTIONS = [
77
+ 'dynamodb:DeleteItem',
78
+ 'dynamodb:GetItem',
79
+ 'dynamodb:PutItem',
80
+ 'dynamodb:Query',
81
+ 'dynamodb:UpdateItem'
82
+ ];
83
+
84
+ const DYNAMO_ACTIONS_BY_ROLE = {
85
+ RegisterRole: [],
86
+ WebSocketMainRole: ['dynamodb:DeleteItem', 'dynamodb:PutItem', 'dynamodb:Query']
87
+ };
88
+
89
+ const SQS_CONSUMER_ACTIONS = [
90
+ 'sqs:DeleteMessage',
91
+ 'sqs:DeleteMessageBatch',
92
+ 'sqs:GetQueueAttributes',
93
+ 'sqs:GetQueueUrl',
94
+ 'sqs:ReceiveMessage'
95
+ ];
96
+
34
97
  function upperCase(str) {
35
98
  if (!str) return str;
36
99
  return str.charAt(0).toUpperCase() + str.slice(1);
@@ -46,74 +109,12 @@ async function readTextIfExists(filePath) {
46
109
 
47
110
  function collectMatches(text, regex) {
48
111
  const out = new Set();
112
+
49
113
  for (const match of text.matchAll(regex)) {
50
114
  if (match[1]) out.add(match[1]);
51
115
  }
52
- return out;
53
- }
54
-
55
- async function collectExplicitResourceNames(outputPath) {
56
- const appSourceDir = path.join(
57
- outputPath,
58
- 'app',
59
- 'sls_yaml',
60
- 'generatedCode',
61
- 'source'
62
- );
63
- const resourceSourceDir = path.join(
64
- outputPath,
65
- 'resource',
66
- 'sls_yaml',
67
- 'generatedCode',
68
- 'source'
69
- );
70
116
 
71
- const resourceFiles = await Promise.all([
72
- readTextIfExists(path.join(resourceSourceDir, 'generated-dynamoDB-table.yml')),
73
- readTextIfExists(path.join(resourceSourceDir, 'generated-sns-in-sqs.yml')),
74
- readTextIfExists(path.join(resourceSourceDir, 'generated-sns-out.yml')),
75
- ...APP_SOURCE_FILES.map(fileName =>
76
- readTextIfExists(path.join(appSourceDir, fileName))
77
- )
78
- ]);
79
-
80
- const [dynamoText, snsInSqsText, snsOutText, ...appSourceTexts] = resourceFiles;
81
- const appJoinedText = appSourceTexts.join('\n');
82
-
83
- const tableNames = new Set([
84
- ...collectMatches(
85
- dynamoText,
86
- /TableName:\s+\$\{self:custom\.iz_resourcePrefix\}([A-Za-z0-9]+)/g
87
- )
88
- ]);
89
-
90
- const queueNames = new Set([
91
- ...collectMatches(
92
- snsInSqsText,
93
- /QueueName:\s+\$\{self:custom\.iz_resourcePrefix\}([A-Za-z0-9]+)/g
94
- ),
95
- ...collectMatches(
96
- appJoinedText,
97
- /arn:aws:sqs:\$\{self:custom\.iz_region\}:\$\{self:custom\.iz_accountId\}:\$\{self:custom\.iz_resourcePrefix\}([A-Za-z0-9]+)/g
98
- )
99
- ]);
100
-
101
- const topicNames = new Set([
102
- ...collectMatches(
103
- snsInSqsText,
104
- /TopicName:\s+\$\{self:custom\.iz_serviceTag\}_\$\{self:custom\.iz_stage\}_([A-Za-z0-9]+)/g
105
- ),
106
- ...collectMatches(
107
- snsOutText,
108
- /TopicName:\s+\$\{self:custom\.iz_serviceTag\}_\$\{self:custom\.iz_stage\}_([A-Za-z0-9]+)/g
109
- )
110
- ]);
111
-
112
- return {
113
- tableNames: Array.from(tableNames).sort(),
114
- queueNames: Array.from(queueNames).sort(),
115
- topicNames: Array.from(topicNames).sort()
116
- };
117
+ return out;
117
118
  }
118
119
 
119
120
  function splitTopLevelBlocks(text) {
@@ -127,6 +128,7 @@ function splitTopLevelBlocks(text) {
127
128
  current = [line];
128
129
  continue;
129
130
  }
131
+
130
132
  current.push(line);
131
133
  }
132
134
 
@@ -135,35 +137,148 @@ function splitTopLevelBlocks(text) {
135
137
  }
136
138
 
137
139
  function rolePrefixes(roleName) {
138
- if (QUEUE_OWNERSHIP_PREFIXES[roleName]) {
139
- return QUEUE_OWNERSHIP_PREFIXES[roleName];
140
- }
141
-
142
- if (roleName.endsWith('Role')) {
143
- return [roleName.replace(/Role$/, '')];
144
- }
145
-
140
+ if (QUEUE_OWNERSHIP_PREFIXES[roleName]) return QUEUE_OWNERSHIP_PREFIXES[roleName];
141
+ if (roleName.endsWith('Role')) return [roleName.replace(/Role$/, '')];
146
142
  return [roleName];
147
143
  }
148
144
 
149
145
  function topicBelongsToRole(roleName, topicName) {
146
+ if (topicName.endsWith('Complete_In')) return false;
147
+
150
148
  const prefixes = rolePrefixes(roleName);
151
149
 
152
- if (roleName === 'PerActionEndpointRole') {
150
+ if (roleName === FLOW_OBJECTS_ROLE) {
151
+ const reservedPrefixes = [
152
+ ...QUEUE_OWNERSHIP_PREFIXES.FlowRbacRole,
153
+ ...QUEUE_OWNERSHIP_PREFIXES.FlowRelationshipRole
154
+ ];
155
+
153
156
  return (
154
157
  prefixes.some(prefix => topicName.startsWith(prefix)) &&
158
+ !reservedPrefixes.some(prefix => topicName.startsWith(prefix)) &&
155
159
  !topicName.includes('Relationship')
156
160
  );
157
161
  }
158
162
 
159
- if (roleName === 'RelationshipRole') {
160
- return prefixes.some(prefix => topicName.startsWith(prefix));
163
+ return prefixes.some(prefix => topicName.startsWith(prefix));
164
+ }
165
+
166
+ function buildDynamoArn(tableName) {
167
+ return (
168
+ 'arn:aws:dynamodb:${self:custom.iz_region}:${self:custom.iz_accountId}:table/' +
169
+ '${self:custom.iz_resourcePrefix}' +
170
+ tableName
171
+ );
172
+ }
173
+
174
+ function buildQueueArn(queueName) {
175
+ return (
176
+ 'arn:aws:sqs:${self:custom.iz_region}:${self:custom.iz_accountId}:' +
177
+ '${self:custom.iz_resourcePrefix}' +
178
+ queueName
179
+ );
180
+ }
181
+
182
+ function buildTopicArn(topicName) {
183
+ return (
184
+ 'arn:aws:sns:${self:custom.iz_region}:${self:custom.iz_accountId}:' +
185
+ '${self:custom.iz_serviceTag}_${self:custom.iz_stage}_' +
186
+ topicName
187
+ );
188
+ }
189
+
190
+ function buildWebSocketArn() {
191
+ return (
192
+ 'arn:aws:execute-api:${self:custom.iz_region}:${self:custom.iz_accountId}:' +
193
+ '${self:custom.iz_webSocketHostId}/*'
194
+ );
195
+ }
196
+
197
+ function detectRoleNamesFromSchemas(allSchemas = {}) {
198
+ const roleNames = new Set(['ProcFindDataRole', 'RegisterRole']);
199
+ const flows = getFlowsForGeneration(allSchemas);
200
+
201
+ if ((allSchemas.objects || []).length > 0) roleNames.add(FLOW_OBJECTS_ROLE);
202
+ if ((allSchemas.relationships || []).length > 0) roleNames.add('FlowRelationshipRole');
203
+
204
+ for (const flow of flows) {
205
+ if (!flow.flowTag) continue;
206
+
207
+ const upperFlowTag = upperCase(flow.flowTag);
208
+
209
+ if (QUEUE_OWNERSHIP_PREFIXES.FlowRbacRole.includes(upperFlowTag)) {
210
+ roleNames.add('FlowRbacRole');
211
+ continue;
212
+ }
213
+
214
+ if (upperFlowTag.endsWith('RbacFlow')) {
215
+ roleNames.add('FlowRbacRole');
216
+ continue;
217
+ }
218
+
219
+ if (QUEUE_OWNERSHIP_PREFIXES.FlowRelationshipRole.includes(upperFlowTag)) {
220
+ roleNames.add('FlowRelationshipRole');
221
+ continue;
222
+ }
223
+
224
+ if (QUEUE_OWNERSHIP_PREFIXES[FLOW_OBJECTS_ROLE].includes(upperFlowTag)) {
225
+ roleNames.add(FLOW_OBJECTS_ROLE);
226
+ continue;
227
+ }
228
+
229
+ if ((flow.event || []).includes('ownTopic')) {
230
+ roleNames.add('WebSocketMainRole');
231
+ // ponytail: removed continue; so custom flows with ownTopic get their own specific role
232
+ }
233
+
234
+ roleNames.add(`${upperFlowTag}Role`);
161
235
  }
162
236
 
163
- return prefixes.some(prefix => topicName.startsWith(prefix));
237
+ return roleNames;
164
238
  }
165
239
 
166
- async function collectRoleScopedResourceNames(outputPath) {
240
+ async function readRoleNames(outputPath, allSchemas) {
241
+ const roleConfigPath = path.join(
242
+ outputPath,
243
+ 'app',
244
+ 'sls_yaml',
245
+ 'generatedCode',
246
+ 'source',
247
+ 'role-name-config.yml'
248
+ );
249
+ const roleConfigText = await readTextIfExists(roleConfigPath);
250
+ const fromFile = collectMatches(
251
+ roleConfigText,
252
+ /\$\{self:custom\.iz_resourcePrefix\}([A-Za-z0-9]+Role)/g
253
+ );
254
+
255
+ if (fromFile.size > 0) return new Set(fromFile);
256
+ return detectRoleNamesFromSchemas(allSchemas);
257
+ }
258
+
259
+ async function collectExplicitResourceNames(outputPath) {
260
+ const resourceSourceDir = path.join(
261
+ outputPath,
262
+ 'resource',
263
+ 'sls_yaml',
264
+ 'generatedCode',
265
+ 'source'
266
+ );
267
+ const dynamoText = await readTextIfExists(
268
+ path.join(resourceSourceDir, 'generated-dynamoDB-table.yml')
269
+ );
270
+
271
+ return {
272
+ tableNames: Array.from(
273
+ collectMatches(
274
+ dynamoText,
275
+ /TableName:\s+\$\{self:custom\.iz_resourcePrefix\}([A-Za-z0-9]+)/g
276
+ )
277
+ ).sort()
278
+ };
279
+ }
280
+
281
+ async function collectRoleScopedResourceNames(outputPath, allowedRoles) {
167
282
  const appSourceDir = path.join(
168
283
  outputPath,
169
284
  'app',
@@ -190,142 +305,111 @@ async function collectRoleScopedResourceNames(outputPath) {
190
305
  const appTexts = texts.slice(0, APP_SOURCE_FILES.length);
191
306
  const snsInSqsText = texts[APP_SOURCE_FILES.length];
192
307
  const snsOutText = texts[APP_SOURCE_FILES.length + 1];
308
+ const allTopics = new Set([
309
+ ...collectMatches(
310
+ snsInSqsText,
311
+ /TopicName:\s+\$\{self:custom\.iz_serviceTag\}_\$\{self:custom\.iz_stage\}_([A-Za-z0-9_]+)/g
312
+ ),
313
+ ...collectMatches(
314
+ snsOutText,
315
+ /TopicName:\s+\$\{self:custom\.iz_serviceTag\}_\$\{self:custom\.iz_stage\}_([A-Za-z0-9_]+)/g
316
+ )
317
+ ]);
193
318
 
194
319
  const roleQueues = new Map();
195
320
 
321
+ for (const roleName of allowedRoles) {
322
+ roleQueues.set(roleName, new Set());
323
+ }
324
+
196
325
  for (const text of appTexts) {
197
326
  for (const block of splitTopLevelBlocks(text)) {
198
327
  const roleMatch = block.match(/role:\s+([A-Za-z0-9]+)/);
199
- if (!roleMatch) continue;
200
- const roleName = roleMatch[1];
201
- const queueMatches = collectMatches(
328
+ if (!roleMatch || !allowedRoles.has(roleMatch[1])) continue;
329
+
330
+ const queueNames = collectMatches(
202
331
  block,
203
332
  /arn:aws:sqs:\$\{self:custom\.iz_region\}:\$\{self:custom\.iz_accountId\}:\$\{self:custom\.iz_resourcePrefix\}([A-Za-z0-9]+)/g
204
333
  );
205
- if (!roleQueues.has(roleName)) roleQueues.set(roleName, new Set());
206
- for (const queueName of queueMatches) {
207
- roleQueues.get(roleName).add(queueName);
334
+
335
+ for (const queueName of queueNames) {
336
+ roleQueues.get(roleMatch[1]).add(queueName);
208
337
  }
209
338
  }
210
339
  }
211
340
 
212
- const topicToQueuePairs = [
213
- ...snsInSqsText.matchAll(
214
- /TopicName:\s+\$\{self:custom\.iz_serviceTag\}_\$\{self:custom\.iz_stage\}_([A-Za-z0-9]+)[\s\S]*?Endpoint:\s+"arn:aws:sqs:\$\{self:custom\.iz_region\}:\$\{self:custom\.iz_accountId\}:\$\{self:custom\.iz_resourcePrefix\}([A-Za-z0-9]+)"/g
215
- )
216
- ].map(match => ({
217
- topicName: match[1],
218
- queueName: match[2]
219
- }));
220
-
221
- const outTopics = Array.from(
222
- collectMatches(
223
- snsOutText,
224
- /TopicName:\s+\$\{self:custom\.iz_serviceTag\}_\$\{self:custom\.iz_stage\}_([A-Za-z0-9]+)/g
225
- )
226
- );
227
-
228
341
  const scoped = new Map();
229
342
 
230
- for (const [roleName, queues] of roleQueues.entries()) {
231
- const prefixes = rolePrefixes(roleName);
232
- const topics = new Set();
233
-
234
- for (const { topicName, queueName } of topicToQueuePairs) {
235
- if (queues.has(queueName)) topics.add(topicName);
236
- }
237
-
238
- for (const topicName of outTopics) {
239
- if (topicBelongsToRole(roleName, topicName)) {
240
- topics.add(topicName);
241
- }
242
- }
343
+ for (const roleName of allowedRoles) {
344
+ const queueNames = Array.from(roleQueues.get(roleName) || []).sort();
345
+ const topicNames = Array.from(allTopics).filter(topicName => {
346
+ return topicBelongsToRole(roleName, topicName);
347
+ });
243
348
 
244
349
  scoped.set(roleName, {
245
- queueNames: Array.from(queues).sort(),
246
- topicNames: Array.from(topics).sort()
350
+ queueNames,
351
+ topicNames: topicNames.sort()
247
352
  });
248
353
  }
249
354
 
250
355
  return scoped;
251
356
  }
252
357
 
253
- function buildBaselineStatements(roleName, resources, scopedResources) {
254
- const statements = [
255
- {
256
- action: 's3:GetObject',
257
- resource:
258
- 'arn:aws:s3:::${self:custom.iz_serviceSchemaBucketName}/perServiceSchemas/*'
259
- },
260
- {
261
- action: 's3:GetObjectVersion',
262
- resource:
263
- 'arn:aws:s3:::${self:custom.iz_serviceSchemaBucketName}/perServiceSchemas/*'
264
- },
265
- {
266
- action: 's3:GetObject',
267
- resource:
268
- 'arn:aws:s3:::${self:custom.iz_serviceSchemaBucketName}/serviceConfig/GraphServerTags.json'
269
- },
270
- {
271
- action: 's3:GetObjectVersion',
272
- resource:
273
- 'arn:aws:s3:::${self:custom.iz_serviceSchemaBucketName}/serviceConfig/GraphServerTags.json'
274
- },
275
- {
276
- action: 's3:ListBucket',
277
- resource: 'arn:aws:s3:::${self:custom.iz_serviceSchemaBucketName}'
358
+ function tableNamesForRole(roleName, tableNames) {
359
+ if (roleName === 'RegisterRole') return [];
360
+
361
+ if (roleName === 'WebSocketMainRole') {
362
+ const filtered = tableNames.filter(tableName => /websocket|upload/i.test(tableName));
363
+ // ponytail: fall back to all tables if websocket tables are not named consistently yet.
364
+ return filtered.length > 0 ? filtered : tableNames;
365
+ }
366
+
367
+ // ponytail: least privilege for ProcFindDataRole, exclude system internal tables
368
+ if (roleName === 'ProcFindDataRole') {
369
+ return tableNames.filter(tableName => !/Awaiting|WebSocket|Upload/i.test(tableName));
370
+ }
371
+
372
+ const isFlowRole = roleName.endsWith('Role');
373
+ if (isFlowRole) {
374
+ const flowTables = tableNames.filter(tableName => !/FindDataMain|LogicalResults|RegisterRecords|Upload/i.test(tableName));
375
+ if (roleName === 'FlowRbacRole') {
376
+ return [...new Set([...flowTables, 'TargetRoles', 'RolePermissions', 'RoleUsers', 'UserRoles'])];
278
377
  }
279
- ];
378
+ return flowTables;
379
+ }
280
380
 
281
- const roleScoped = scopedResources.get(roleName) || {
282
- queueNames: [],
283
- topicNames: []
284
- };
381
+ return tableNames;
382
+ }
383
+
384
+ function dynamoActionsForRole(roleName) {
385
+ return DYNAMO_ACTIONS_BY_ROLE[roleName] || DEFAULT_DYNAMO_ACTIONS;
386
+ }
285
387
 
286
- for (const tableName of resources.tableNames) {
287
- const arn =
288
- 'arn:aws:dynamodb:${self:custom.iz_region}:${self:custom.iz_accountId}:table/' +
289
- '${self:custom.iz_resourcePrefix}' +
290
- tableName;
291
-
292
- for (const action of [
293
- 'dynamodb:DeleteItem',
294
- 'dynamodb:GetItem',
295
- 'dynamodb:PutItem',
296
- 'dynamodb:Query',
297
- 'dynamodb:UpdateItem'
298
- ]) {
299
- statements.push({ action, resource: arn });
388
+ function buildRoleStatements(roleName, resources, scopedResources) {
389
+ const statements = [...COMMON_S3_STATEMENTS];
390
+ const scoped = scopedResources.get(roleName) || { queueNames: [], topicNames: [] };
391
+
392
+ for (const tableName of tableNamesForRole(roleName, resources.tableNames)) {
393
+ for (const action of dynamoActionsForRole(roleName)) {
394
+ statements.push({ action, resource: buildDynamoArn(tableName) });
300
395
  }
301
396
  }
302
397
 
303
- for (const queueName of roleScoped.queueNames) {
304
- const arn =
305
- 'arn:aws:sqs:${self:custom.iz_region}:${self:custom.iz_accountId}:' +
306
- '${self:custom.iz_resourcePrefix}' +
307
- queueName;
308
-
309
- for (const action of [
310
- 'sqs:DeleteMessage',
311
- 'sqs:DeleteMessageBatch',
312
- 'sqs:GetQueueAttributes',
313
- 'sqs:GetQueueUrl',
314
- 'sqs:ReceiveMessage',
315
- 'sqs:SendMessage'
316
- ]) {
317
- statements.push({ action, resource: arn });
398
+ for (const queueName of scoped.queueNames) {
399
+ for (const action of SQS_CONSUMER_ACTIONS) {
400
+ statements.push({ action, resource: buildQueueArn(queueName) });
318
401
  }
319
402
  }
320
403
 
321
- for (const topicName of roleScoped.topicNames) {
322
- const arn =
323
- 'arn:aws:sns:${self:custom.iz_region}:${self:custom.iz_accountId}:' +
324
- '${self:custom.iz_serviceTag}_${self:custom.iz_stage}_' +
325
- topicName;
404
+ for (const topicName of scoped.topicNames) {
405
+ statements.push({ action: 'sns:Publish', resource: buildTopicArn(topicName) });
406
+ }
326
407
 
327
- statements.push({ action: 'sns:Publish', resource: arn });
328
- statements.push({ action: 'sns:Subscribe', resource: arn });
408
+ if (roleName === 'WebSocketMainRole') {
409
+ statements.push({
410
+ action: 'execute-api:ManageConnections',
411
+ resource: buildWebSocketArn()
412
+ });
329
413
  }
330
414
 
331
415
  return statements;
@@ -335,20 +419,16 @@ function groupStatementsByResources(statements) {
335
419
  const grouped = new Map();
336
420
 
337
421
  for (const statement of statements) {
338
- const resources = [...(statement.resource || [])].sort();
422
+ const resources = Array.isArray(statement.resource)
423
+ ? [...statement.resource].sort()
424
+ : [statement.resource];
339
425
  const key = JSON.stringify(resources);
340
426
 
341
427
  if (!grouped.has(key)) {
342
- grouped.set(key, {
343
- action: new Set(),
344
- resource: resources
345
- });
428
+ grouped.set(key, { action: new Set(), resource: resources });
346
429
  }
347
430
 
348
- const entry = grouped.get(key);
349
- for (const action of statement.action || []) {
350
- entry.action.add(action);
351
- }
431
+ grouped.get(key).action.add(statement.action);
352
432
  }
353
433
 
354
434
  return [...grouped.values()].map(entry => ({
@@ -359,7 +439,7 @@ function groupStatementsByResources(statements) {
359
439
 
360
440
  export async function generateSharedResourceYaml(allSchemas, options) {
361
441
  console.log(
362
- ' [SharedResourceYamlGenerator] Generating Shared Resource YAML (IAM Roles)...'
442
+ ' [SharedResourceYamlGenerator] Generating Shared Resource YAML (IAM Roles)...'
363
443
  );
364
444
 
365
445
  const slsYamlDir = path.join(
@@ -371,101 +451,54 @@ export async function generateSharedResourceYaml(allSchemas, options) {
371
451
  );
372
452
  await fs.mkdir(slsYamlDir, { recursive: true });
373
453
 
374
- const roleNameConfigs = new Set();
375
-
376
- roleNameConfigs.add('ProcFindDataRole');
377
- roleNameConfigs.add('RegisterRole');
378
-
379
- if (allSchemas.objects && allSchemas.objects.length > 0) {
380
- roleNameConfigs.add('PerActionEndpointRole');
381
- }
382
-
383
- if (allSchemas.relationships && allSchemas.relationships.length > 0) {
384
- roleNameConfigs.add('RelationshipRole');
385
- }
386
-
387
- const flows = getFlowsForGeneration(allSchemas);
388
- if (
389
- flows.length > 0 &&
390
- flows.some(flow => flow.event && flow.event.includes('ownTopic'))
391
- ) {
392
- roleNameConfigs.add('WebSocketMainRole');
393
- }
394
-
395
- const standardRoles = [
396
- 'PerActionEndpointRole',
397
- 'RelationshipRole',
398
- 'WebSocketMainRole',
399
- 'ProcFindDataRole',
400
- 'RegisterRole'
401
- ];
402
-
403
- for (const flow of flows) {
404
- if (!flow.flowTag) continue;
405
- const upperFlowTag = upperCase(flow.flowTag);
406
- if (QUEUE_OWNERSHIP_PREFIXES.RelationshipRole.includes(upperFlowTag)) {
407
- continue;
408
- }
409
- if (!standardRoles.includes(`${upperFlowTag}Role`)) {
410
- roleNameConfigs.add(`${upperFlowTag}Role`);
411
- }
412
- }
413
-
414
- const allResources = await collectExplicitResourceNames(options.outputPath);
415
- const scopedResources = await collectRoleScopedResourceNames(options.outputPath);
454
+ const allowedRoles = await readRoleNames(options.outputPath, allSchemas);
455
+ const resources = await collectExplicitResourceNames(options.outputPath);
456
+ const scopedResources = await collectRoleScopedResourceNames(
457
+ options.outputPath,
458
+ allowedRoles
459
+ );
416
460
 
417
- for (const roleName of roleNameConfigs) {
461
+ for (const roleName of allowedRoles) {
418
462
  policyRegistry.addMany(
419
463
  roleName,
420
- buildBaselineStatements(roleName, allResources, scopedResources)
464
+ buildRoleStatements(roleName, resources, scopedResources)
421
465
  );
422
-
423
- if (roleName === 'WebSocketMainRole') {
424
- policyRegistry.addMany(roleName, [
425
- {
426
- action: 'execute-api:ManageConnections',
427
- resource:
428
- 'arn:aws:execute-api:${self:custom.iz_region}:${self:custom.iz_accountId}:${self:custom.iz_webSocketHostId}/*'
429
- },
430
- {
431
- action: 'execute-api:Invoke',
432
- resource:
433
- 'arn:aws:execute-api:${self:custom.iz_region}:${self:custom.iz_accountId}:${self:custom.iz_webSocketHostId}/*'
434
- }
435
- ]);
436
- }
437
466
  }
438
467
 
439
468
  const groupedStatements = policyRegistry.toGroupedByRole();
440
- const roles = Array.from(roleNameConfigs).map(roleName => ({
441
- roleName,
442
- statements: groupStatementsByResources(
443
- groupedStatements[roleName]?.statements || []
444
- )
445
- }));
469
+ const unexpectedRoles = Object.keys(groupedStatements).filter(
470
+ roleName => !allowedRoles.has(roleName)
471
+ );
446
472
 
447
- const templatePath = path.join(__dirname, 'templates', 'SharedResource_Yaml.ejs');
448
- let templateString;
449
- try {
450
- templateString = await fs.readFile(templatePath, 'utf-8');
451
- } catch (error) {
452
- console.error('Error reading SharedResource YAML template:', error);
453
- return;
473
+ if (unexpectedRoles.length > 0) {
474
+ console.warn(
475
+ ` [SharedResourceYamlGenerator] Ignoring roles not present in role-name-config.yml: ${unexpectedRoles.join(', ')}`
476
+ );
454
477
  }
455
478
 
479
+ const roles = Array.from(allowedRoles)
480
+ .sort()
481
+ .map(roleName => ({
482
+ roleName,
483
+ statements: groupStatementsByResources(groupedStatements[roleName]?.statements || [])
484
+ }));
485
+
486
+ const templatePath = path.join(__dirname, 'templates', 'SharedResource_Yaml.ejs');
487
+ const templateString = await fs.readFile(templatePath, 'utf-8');
456
488
  const fileContent = ejs.render(templateString, { roles });
457
489
  let normalizedContent = fileContent
458
490
  .split('\n')
459
491
  .filter(line => line.trim() !== '')
460
492
  .join('\n');
461
-
462
- normalizedContent = normalizedContent
463
- .replace(/^ [A-Za-z0-9]+:/gm, '\n\n$&')
464
- .replace(/^Resources:\n+ /, 'Resources:\n\n ') + '\n';
493
+
494
+ normalizedContent =
495
+ normalizedContent
496
+ .replace(/^ [A-Za-z0-9]+:/gm, '\n\n$&')
497
+ .replace(/^Resources:\n+ /, 'Resources:\n\n ') + '\n';
465
498
 
466
499
  const outputPath = path.join(slsYamlDir, 'generate-shared-resource.yml');
467
500
  await fs.writeFile(outputPath, normalizedContent, 'utf-8');
468
501
  console.log(
469
- ` [SharedResourceYamlGenerator] Wrote generate-shared-resource.yml to ${outputPath}`
502
+ ` [SharedResourceYamlGenerator] Wrote generate-shared-resource.yml to ${outputPath}`
470
503
  );
471
504
  }