@cloud-copilot/iam-expand 0.1.9 → 0.1.10

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.
@@ -1,489 +0,0 @@
1
- import { iamActionDetails, iamActionExists, iamActionsForService, iamServiceExists, iamServiceKeys } from '@cloud-copilot/iam-data'
2
- import { beforeEach, describe, expect, it, vi } from 'vitest'
3
- import { expandIamActions, InvalidActionBehavior } from "./expand.js"
4
-
5
- vi.mock('@cloud-copilot/iam-data')
6
-
7
- beforeEach(() => {
8
- vi.resetAllMocks()
9
- // jest.resetAllMocks()
10
- })
11
-
12
- describe("expand", () => {
13
- it("should return an empty array when actionString is null", async () => {
14
- //Given actionString is null
15
- const actionString = null
16
- //When expand is called with actionString
17
- const result = await expandIamActions(actionString as any)
18
- //Then result should be an empty array
19
- expect(result).toEqual([])
20
- })
21
-
22
- it("should return '*' when actionString is '*' and expandAsterisk is false", async () => {
23
- //Given actionString is '*'
24
- const actionString = '*'
25
- //When expand is called with actionString
26
- const result = await expandIamActions(actionString)
27
- //Then result should be '*'
28
- expect(result).toEqual(['*'])
29
- })
30
-
31
- it('should return "*" when action string multiple asterisks and expandAsterisk is false', async () => {
32
- //Given actionString is multiple asterisks
33
- const actionString = '***'
34
-
35
- //And expandAsterisk is false
36
- const options = { expandAsterisk: false }
37
-
38
- //When expand is called with actionString and options
39
- const result = await expandIamActions(actionString, options)
40
-
41
- //Then result should be '*'
42
- expect(result).toEqual(['*'])
43
- })
44
-
45
- it("should expand all actions for all services when actionString is '*' and expandAsterisk is true", async () => {
46
- //Given actionString is '*'
47
- const actionString = '*'
48
- //And expandAsterisk is true
49
- const options = { expandAsterisk: true }
50
- //And there are services
51
- vi.mocked(iamServiceKeys).mockResolvedValue(['s3', 'ec2'])
52
-
53
- //And there are actions for the services
54
- vi.mocked(iamActionsForService).mockImplementation(async (service) => {
55
- if(service === 's3') {
56
- return ['action1', 'action2']
57
- }
58
- if(service === 'ec2') {
59
- return ['action3', 'action4']
60
- }
61
- return []
62
- })
63
-
64
- //When expand is called with actionString and options
65
- const result = await expandIamActions(actionString, options)
66
- //Then result should be an array of all actions for all services
67
- expect(result.sort()).toEqual([
68
- 'ec2:action3',
69
- 'ec2:action4',
70
- 's3:action1',
71
- 's3:action2'
72
- ])
73
- })
74
-
75
- it("should do a case insensitive match for the service in the action string", async () => {
76
- //Given actionString is 'S3:GetObject'
77
- const actionString = 'S3:get*'
78
- //And s3 service exists
79
- vi.mocked(iamServiceExists).mockImplementation(async (s) => s === 's3')
80
- //And there are matching actions
81
- vi.mocked(iamActionsForService).mockResolvedValue(['GetObject'])
82
-
83
- //When expand is called with actionString
84
- const result = await expandIamActions(actionString)
85
-
86
- //Then result should be an array with the actionString
87
- expect(result).toEqual(['s3:GetObject'])
88
- })
89
-
90
- describe("invalid action name", () => {
91
- it('should return an action without wildcards if it is a valid action', async () => {
92
- //Given actionString contains a valid action
93
- const actionString = 's3:getobject'
94
- //And s3 the service exists
95
- vi.mocked(iamServiceExists).mockResolvedValue(true)
96
- //And the action does not
97
- vi.mocked(iamActionExists).mockResolvedValue(true)
98
- vi.mocked(iamActionDetails).mockResolvedValue({name: 'GetObject'} as any)
99
-
100
- //When expand is called with actionString
101
- const result = await expandIamActions(actionString)
102
-
103
- //Then result should be an array with the actionString
104
- expect(result).toEqual(['s3:GetObject'])
105
- })
106
-
107
- it("should remove an invalid action if invalidActionBehavior is Remove", async () => {
108
- //Given actionString contains an invalid action
109
- const actionString = 's3:DoSomethingDumb'
110
- //And invalidActionBehavior is Remove
111
- const options = { invalidActionBehavior: InvalidActionBehavior.Remove }
112
- //And s3 the service exists
113
- vi.mocked(iamServiceExists).mockResolvedValue(true)
114
- //And the action does not
115
- vi.mocked(iamActionExists).mockResolvedValue(false)
116
-
117
- //When expand is called with actionString
118
- const result = await expandIamActions(actionString, options)
119
-
120
- //Then result should be an array with the valid action
121
- expect(result).toEqual([])
122
- })
123
-
124
- it("should include an invalid action if invalidActionBehavior is Include", async () => {
125
- //Given actionString contains an invalid action
126
- const actionString = 's3:DoSomethingSilly'
127
- //And invalidActionBehavior is Include
128
- const options = { invalidActionBehavior: InvalidActionBehavior.Include }
129
- //And s3 the service exists
130
- vi.mocked(iamServiceExists).mockResolvedValue(true)
131
- //And the action does not
132
- vi.mocked(iamActionExists).mockResolvedValue(false)
133
-
134
- //When expand is called with actionString
135
- const result = await expandIamActions(actionString, options)
136
-
137
- //Then result should be an array with the invalid action
138
- expect(result).toEqual([actionString])
139
- })
140
-
141
- it('should throw an error if the action is invalid and invalidActionBehavior is Error', async () => {
142
- //Given actionString contains an invalid action
143
- const actionString = 's3:AbsurdlyInvalidAction'
144
- //And invalidActionBehavior is Error
145
- const options = { invalidActionBehavior: InvalidActionBehavior.Error }
146
- //And s3 the service exists
147
- vi.mocked(iamServiceExists).mockResolvedValue(true)
148
- //And the action does not
149
- vi.mocked(iamActionExists).mockResolvedValue(false)
150
-
151
- //When expand is called with actionString
152
- //Then an error should be thrown
153
- expect(
154
- expandIamActions(actionString, options)
155
- ).rejects.toThrowError('Invalid action')
156
- })
157
- })
158
-
159
- describe("when the actions string is in the wrong format", () => {
160
- it("should return an empty array when there are too many parts and errorOnInvalidFormat is false", async () => {
161
- //Given actionString is in the wrong format
162
- const actionString = 's3:GetObject:Extra*'
163
- //And errorOnInvalidFormat is false
164
- const options = { errorOnInvalidFormat: false }
165
-
166
- //When expand is called with actionString
167
- const result = await expandIamActions(actionString, options)
168
-
169
- //Then result should be an empty array
170
- expect(result).toEqual([])
171
- })
172
-
173
- it("should return an empty array when there are too few parts and errorOnInvalidFormat is false", async () => {
174
- //Given actionString has no :
175
- const actionString = 's3GetObject*'
176
- //And errorOnInvalidFormat is false
177
- const options = { errorOnInvalidFormat: false }
178
-
179
- //When expand is called with actionString
180
- const result = await expandIamActions(actionString, options)
181
-
182
- //Then result should be an empty array
183
- expect(result).toEqual([])
184
- })
185
-
186
- it("should throw an error when there are too many parts and errorOnInvalidFormat is true", async () => {
187
- //Given actionString is in the wrong format
188
- const actionString = 's3:GetObject:Extra*'
189
- //And errorOnInvalidFormat is true
190
- const options = { errorOnInvalidFormat: true }
191
-
192
- //When expand is called with actionString
193
- //Then an error should be thrown
194
- expect(
195
- () => expandIamActions(actionString, options)
196
- ).rejects.toThrowError('Invalid action format')
197
- })
198
-
199
- it("should throw an error when there are too few parts and errorOnInvalidFormat is true", async () => {
200
- //Given actionString has no :
201
- const actionString = 's3GetObject*'
202
- //And errorOnInvalidFormat is true
203
- const options = { errorOnInvalidFormat: true }
204
-
205
- //When expand is called with actionString
206
- //Then an error should be thrown
207
- expect(
208
- () => expandIamActions(actionString, options)
209
- ).rejects.toThrowError('Invalid action format')
210
- })
211
- })
212
-
213
- describe("when the service in the action string does not exist", () => {
214
- it("should return an empty array when errorOnInvalidService is false", async () => {
215
- //Given actionString contains a service that does not exist
216
- const actionString = 'fake:GetObject*'
217
- //And errorOnMissingService is false
218
- const options = { errorOnInvalidService: false }
219
-
220
- //When expand is called with actionString
221
- const result = await expandIamActions(actionString, options)
222
-
223
- //Then result should be an empty array
224
- expect(result).toEqual([])
225
- })
226
-
227
- it("should throw an error when errorOnInvalidService is true", async () => {
228
- //Given actionString contains a service that does not exist
229
- const actionString = 'fake:GetObject*'
230
- //And errorOnMissingService is true
231
- const options = { errorOnInvalidService: true }
232
-
233
- //When expand is called with actionString
234
- //Then an error should be thrown
235
- expect(
236
- () => expandIamActions(actionString, options)
237
- ).rejects.toThrowError('Service not found')
238
- })
239
- })
240
-
241
- describe("when the action string contains a wildcard for a service", () => {
242
- it("should expand not expand the wildcard when expandServiceAsterisk is false", async () => {
243
- //Given actionString is 's3:*'
244
- const actionString = 's3:*'
245
- //And expandServiceAsterisk is false
246
- const options = { expandServiceAsterisk: false }
247
- //And s3 service exists
248
- vi.mocked(iamServiceExists).mockResolvedValue(true)
249
- //And there are matching actions
250
- vi.mocked(iamActionsForService).mockResolvedValue(['GetObject', 'PutObject'])
251
-
252
- //When expand is called with actionString
253
- const result = await expandIamActions(actionString, options)
254
-
255
- //Then result should be an array with the original string
256
- expect(result).toEqual([actionString])
257
- })
258
-
259
- it("should expand not expand the wildcard when there are multiple asterisks and expandServiceAsterisk is false", async () => {
260
- //Given actionString has multiple asterisks for the actions
261
- const actionString = 's3:****'
262
- //And expandServiceAsterisk is false
263
- const options = { expandServiceAsterisk: false }
264
- //And s3 service exists
265
- vi.mocked(iamServiceExists).mockResolvedValue(true)
266
- //And there are matching actions
267
- vi.mocked(iamActionsForService).mockResolvedValue(['GetObject', 'PutObject'])
268
-
269
- //When expand is called with actionString
270
- const result = await expandIamActions(actionString, options)
271
-
272
- //Then result should be an array with the original string
273
- expect(result).toEqual(['s3:*'])
274
- })
275
-
276
- it("should expand the wildcard when expandServiceAsterisk is true", async () => {
277
- //Given actionString is 's3:*'
278
- const actionString = 's3:*'
279
- //And expandServiceAsterisk is true
280
- const options = { expandServiceAsterisk: true }
281
- //And s3 service exists
282
- vi.mocked(iamServiceExists).mockResolvedValue(true)
283
- //And there are matching actions
284
- vi.mocked(iamActionsForService).mockResolvedValue(['GetObject', 'PutObject'])
285
-
286
- //When expand is called with actionString
287
- const result = await expandIamActions(actionString, options)
288
-
289
- //Then result should be an array of actions
290
- expect(result).toEqual([
291
- 's3:GetObject',
292
- 's3:PutObject'
293
- ])
294
- })
295
- })
296
-
297
-
298
- describe("when the action string contains wildcards", () => {
299
- it('should expand the wildcard actions at the end', async () => {
300
- //Given actionString is 's3:Get*'
301
- const actionString = 's3:Get*'
302
- //And s3 service exists
303
- vi.mocked(iamServiceExists).mockResolvedValue(true)
304
- //And there are matching actions
305
- vi.mocked(iamActionsForService).mockResolvedValue([
306
- 'GetObject',
307
- 'GetObjectAcl',
308
- 'GetObjectTagging',
309
- 'GetObjectTorrent',
310
- 'PutObject',
311
- 'PutObjectAcl',
312
- 'SomethingGetSomething'
313
- ])
314
-
315
- //When expand is called with actionString
316
- const result = await expandIamActions(actionString)
317
- //Then result should be an array of actions
318
- expect(result).toEqual([
319
- 's3:GetObject',
320
- 's3:GetObjectAcl',
321
- 's3:GetObjectTagging',
322
- 's3:GetObjectTorrent'
323
- ])
324
- })
325
-
326
- it('should expand the wildcard actions at the beginning', async () => {
327
- //Given actionString is 's3:*Object'
328
- const actionString = 's3:*Object'
329
- //And s3 service exists
330
- vi.mocked(iamServiceExists).mockResolvedValue(true)
331
- //And there are matching actions
332
- vi.mocked(iamActionsForService).mockResolvedValue([
333
- 'GetObject',
334
- 'GetObjectAcl',
335
- 'GetObjectTagging',
336
- 'GetObjectTorrent',
337
- 'PutObject',
338
- 'PutObjectAcl',
339
- 'SomethingGetSomething'
340
- ])
341
-
342
- //When expand is called with actionString
343
- const result = await expandIamActions(actionString)
344
- //Then result should be an array of actions
345
- expect(result).toEqual([
346
- 's3:GetObject',
347
- 's3:PutObject'
348
- ])
349
- })
350
-
351
- it('should expand the wildcard actions in the middle', async () => {
352
- //Given actionString is 's3:Get*Tagging'
353
- const actionString = 's3:Get*Tagging'
354
- //And s3 service exists
355
- vi.mocked(iamServiceExists).mockResolvedValue(true)
356
- //And there are matching actions
357
- vi.mocked(iamActionsForService).mockResolvedValue([
358
- 'GetObject',
359
- 'GetObjectAcl',
360
- 'GetObjectTagging',
361
- 'GetBanskyTagging',
362
- 'GetObjectTorrent',
363
- 'PutObject',
364
- 'PutObjectAcl',
365
- 'SomethingGetSomething'
366
- ])
367
-
368
- //When expand is called with actionString
369
- const result = await expandIamActions(actionString)
370
- //Then result should be an array of actions
371
- expect(result).toEqual([
372
- 's3:GetBanskyTagging',
373
- 's3:GetObjectTagging'
374
- ])
375
- })
376
-
377
- it('should expand multiple wildcards', async () => {
378
- //Given actionString is 's3:Get*Tagging*'
379
- const actionString = 's3:Get*Tagging*'
380
- //And s3 service exists
381
- vi.mocked(iamServiceExists).mockResolvedValue(true)
382
- //And there are matching actions
383
- vi.mocked(iamActionsForService).mockResolvedValue([
384
- 'GetObject',
385
- 'GetObjectAcl',
386
- 'GetObjectTagging',
387
- 'GetBanskyTagging',
388
- 'GetTagging',
389
- 'GetObjectTorrent',
390
- 'GetSomethingTaggingSomething',
391
- 'PutObject',
392
- 'PutObjectAcl',
393
- 'SomethingGetSomething'
394
- ])
395
-
396
- //When expand is called with actionString
397
- const result = await expandIamActions(actionString)
398
- //Then result should be an array of actions
399
- expect(result).toEqual([
400
- 's3:GetBanskyTagging',
401
- 's3:GetObjectTagging',
402
- 's3:GetSomethingTaggingSomething',
403
- 's3:GetTagging'
404
- ])
405
- })
406
- })
407
-
408
- describe("when actionStrings is an array", () => {
409
- it("should return an empty array when actionStrings is an empty array", async () => {
410
- //Given actionStrings is an empty array
411
- const actionStrings: string[] = []
412
-
413
- //When expand is called with actionStrings
414
- const result = await expandIamActions(actionStrings)
415
-
416
- //Then result should be an empty array
417
- expect(result).toEqual([])
418
- })
419
-
420
- it("should return an array of expanded actions when actionStrings is an array of action strings", async () => {
421
- //Given actionStrings is an array of action strings
422
- const actionStrings = [
423
- 's3:Get*',
424
- 'ec2:*Instances'
425
- ]
426
- //And s3 and ec2 services exist
427
- vi.mocked(iamServiceExists).mockResolvedValue(true)
428
- //And there are actions for the services
429
- vi.mocked(iamActionsForService).mockImplementation(async (service) => {
430
- if(service === 's3') {
431
- return ['GetObject', 'GetObjectTagging', 'PutObject', 'PutObjectTagging']
432
- }
433
- if(service === 'ec2') {
434
- return ['RunInstances', 'TerminateInstances']
435
- }
436
- return []
437
- })
438
-
439
- //When expand is called with actionStrings
440
- const result = await expandIamActions(actionStrings)
441
-
442
- //Then result should be an array of expanded actions
443
- expect(result.sort()).toEqual([
444
- 'ec2:RunInstances',
445
- 'ec2:TerminateInstances',
446
- 's3:GetObject',
447
- 's3:GetObjectTagging',
448
- ])
449
- })
450
- })
451
-
452
- it('should return only unique values', async () => {
453
- //Given two action strings
454
- const actionString = ['s3:Get*','s3:*Object']
455
- //And s3 service exists
456
- vi.mocked(iamServiceExists).mockResolvedValue(true)
457
- //And there are matching actions
458
- vi.mocked(iamActionsForService).mockResolvedValue(['GetObject', 'PutObject', 'GetOtherObject'])
459
-
460
- //When expand is called with actionStrings and distinct is true
461
- const result = await expandIamActions(actionString)
462
- //Then result should be an array of unique actions
463
- expect(result).toEqual(['s3:GetObject', 's3:GetOtherObject', 's3:PutObject'])
464
- })
465
-
466
- it('should return values sorted', async () => {
467
- //Given two action strings
468
- const actionString = ['s3:Get*','ec2:Describe*']
469
- //And s3 service exists
470
- vi.mocked(iamServiceExists).mockResolvedValue(true)
471
- //And there are matching actions
472
- vi.mocked(iamActionsForService).mockImplementation(async (service) => {
473
- if(service === 's3') {
474
- return ['GetObject', 'GetBucket']
475
- }
476
- if(service === 'ec2') {
477
- return ['DescribeInstances', 'DescribeVolumes']
478
- }
479
- return []
480
- })
481
-
482
- //When expand is called with actionStrings
483
- const result = await expandIamActions(actionString)
484
-
485
- //Then result should be an array of sorted actions
486
- expect(result).toEqual(['ec2:DescribeInstances', 'ec2:DescribeVolumes', 's3:GetBucket', 's3:GetObject'])
487
- })
488
-
489
- })
package/src/expand.ts DELETED
@@ -1,166 +0,0 @@
1
- import { iamActionDetails, iamActionExists, iamActionsForService, iamServiceExists, iamServiceKeys } from '@cloud-copilot/iam-data'
2
-
3
- export enum InvalidActionBehavior {
4
- Remove = "Remove",
5
- Error = "Error",
6
- Include = "Include",
7
- }
8
-
9
- /**
10
- * Options for the expand function
11
- *
12
- */
13
- export interface ExpandIamActionsOptions {
14
- /**
15
- * If true, a single `*` will be expanded to all actions for all services
16
- * If false, a single `*` will be returned as is
17
- * Default: false
18
- */
19
- expandAsterisk: boolean
20
-
21
- /**
22
- * If true, `service:*` will be expanded to all actions for that service
23
- * If false, `service:*` will be returned as is
24
- * Default: false
25
- */
26
- expandServiceAsterisk: boolean
27
-
28
- /**
29
- * If true, an error will be thrown if the action string is not in the correct format
30
- * If false, an empty array will be returned
31
- * Default: false
32
- */
33
- errorOnInvalidFormat: boolean
34
-
35
- /**
36
- * If true, an error will be thrown if the service in the action string does not exist
37
- * If false, an empty array will be returned
38
- * Default: false
39
- */
40
- errorOnInvalidService: boolean
41
-
42
- /**
43
- * The behavior to use when an invalid action is encountered without wildcards
44
- * @{InvalidActionBehavior.Remove} will remove the invalid action from the output
45
- * @{InvalidActionBehavior.Error} will throw an error if an invalid action is encountered
46
- * @{InvalidActionBehavior.Include} will include the invalid action in the output
47
- *
48
- * Default: InvalidActionBehavior.Remove
49
- */
50
- invalidActionBehavior: InvalidActionBehavior
51
- }
52
-
53
- const defaultOptions: ExpandIamActionsOptions = {
54
- expandAsterisk: false,
55
- expandServiceAsterisk: false,
56
- errorOnInvalidFormat: false,
57
- errorOnInvalidService: false,
58
- invalidActionBehavior: InvalidActionBehavior.Remove,
59
- }
60
-
61
- const allAsterisksPattern = /^\*+$/i
62
-
63
- /**
64
- * Expands an IAM action string that contains wildcards.
65
- * If the action string contains no wildcards, it is returned as is.
66
- * @see {@link ExpandIamActionsOptions} for options to customize behavior
67
- *
68
- * If any options are set to throw an error, the function will throw an error if validation fails for a single value.
69
- *
70
- * @param actionStringOrStrings An IAM action or array IAM action(s) that may contain wildcards
71
- * @param overrideOptions Options to override the default behavior
72
- * @returns An array of expanded action strings flattend to a single array
73
- */
74
- export async function expandIamActions(actionStringOrStrings: string | string[], overrideOptions?: Partial<ExpandIamActionsOptions>): Promise<string[]> {
75
- const options = {...defaultOptions, ...overrideOptions}
76
-
77
- if(!actionStringOrStrings) {
78
- //Just in case the user passes in null or undefined
79
- return []
80
- }
81
-
82
- if(Array.isArray(actionStringOrStrings)) {
83
- const actionLists = await Promise.all(actionStringOrStrings.map(async (actionString) => {
84
- return expandIamActions(actionString, options);
85
- }))
86
-
87
- const allMatches = Array.from(new Set(actionLists.flat()))
88
- allMatches.sort()
89
-
90
- return allMatches
91
- }
92
-
93
- const actionString = actionStringOrStrings.trim()
94
-
95
- if(actionString.match(allAsterisksPattern)) {
96
- if(options.expandAsterisk) {
97
- //If that's really what you want...
98
- const allActions = []
99
- const serviceKeys = await iamServiceKeys()
100
- for await (const service of serviceKeys) {
101
- const serviceActions = await iamActionsForService(service)
102
- allActions.push(...serviceActions.map(action => `${service}:${action}`))
103
- }
104
- return allActions
105
- }
106
- return ['*']
107
- }
108
-
109
- if(!actionString.includes(':')) {
110
- if(options.errorOnInvalidFormat) {
111
- throw new Error(`Invalid action format: ${actionString}`)
112
- }
113
- return []
114
- }
115
-
116
- const parts = actionString.split(':')
117
- if(parts.length !== 2) {
118
- if(options.errorOnInvalidFormat) {
119
- throw new Error(`Invalid action format: ${actionString}`)
120
- }
121
- return []
122
- }
123
-
124
- const [service, wildcardActions] = parts.map(part => part.toLowerCase())
125
- if(!await iamServiceExists(service)) {
126
- if(options.errorOnInvalidService) {
127
- throw new Error(`Service not found: ${service}`)
128
- }
129
- return []
130
- }
131
-
132
- if(wildcardActions.match(allAsterisksPattern)) {
133
- if(options.expandServiceAsterisk) {
134
- const actionsForService = await iamActionsForService(service)
135
- return actionsForService.map(action => `${service}:${action}`)
136
- }
137
- return [`${service}:*`]
138
- }
139
-
140
- if(!actionString.includes('*')) {
141
- const actionExists = await iamActionExists(service, wildcardActions)
142
- if(actionExists) {
143
- const details = await iamActionDetails(service, wildcardActions)
144
- return [service + ":" + details.name]
145
- }
146
-
147
- if(options.invalidActionBehavior === InvalidActionBehavior.Remove) {
148
- return []
149
- } else if(options.invalidActionBehavior === InvalidActionBehavior.Include) {
150
- return [actionString]
151
- } else if(options.invalidActionBehavior === InvalidActionBehavior.Error) {
152
- throw new Error(`Invalid action: ${actionString}`)
153
- } else {
154
- //This should never happen
155
- throw new Error(`Invalid invalidActionBehavior: ${options.invalidActionBehavior}`)
156
- }
157
- }
158
-
159
- const allActions = await iamActionsForService(service)
160
- const pattern = "^" + wildcardActions.replace(/\*/g, '.*?') + "$"
161
- const regex = new RegExp(pattern, 'i')
162
- const matchingActions = allActions.filter(action => regex.test(action)).map(action => `${service}:${action}`)
163
- matchingActions.sort()
164
-
165
- return matchingActions
166
- }