@cloud-copilot/iam-expand 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/expand.ts CHANGED
@@ -16,14 +16,14 @@ export interface ExpandIamActionsOptions {
16
16
  * If false, a single `*` will be returned as is
17
17
  * Default: false
18
18
  */
19
- expandAsterik: boolean
19
+ expandAsterisk: boolean
20
20
 
21
21
  /**
22
22
  * If true, `service:*` will be expanded to all actions for that service
23
23
  * If false, `service:*` will be returned as is
24
24
  * Default: false
25
25
  */
26
- expandServiceAsterik: boolean
26
+ expandServiceAsterisk: boolean
27
27
 
28
28
  /**
29
29
  * If true, an error will be thrown if the action string is not in the correct format
@@ -66,8 +66,8 @@ export interface ExpandIamActionsOptions {
66
66
  }
67
67
 
68
68
  const defaultOptions: ExpandIamActionsOptions = {
69
- expandAsterik: false,
70
- expandServiceAsterik: false,
69
+ expandAsterisk: false,
70
+ expandServiceAsterisk: false,
71
71
  errorOnInvalidFormat: false,
72
72
  errorOnMissingService: false,
73
73
  invalidActionBehavior: InvalidActionBehavior.Remove,
@@ -75,7 +75,7 @@ const defaultOptions: ExpandIamActionsOptions = {
75
75
  sort: false
76
76
  }
77
77
 
78
- const allAsteriksPattern = /^\*+$/i
78
+ const allAsterisksPattern = /^\*+$/i
79
79
 
80
80
  /**
81
81
  * Expands an IAM action string that contains wildcards.
@@ -88,7 +88,7 @@ const allAsteriksPattern = /^\*+$/i
88
88
  * @param overrideOptions Options to override the default behavior
89
89
  * @returns An array of expanded action strings flattend to a single array
90
90
  */
91
- export function expandIamActions(actionStringOrStrings: string | string[], overrideOptions?: Partial<ExpandIamActionsOptions>): string[] {
91
+ export async function expandIamActions(actionStringOrStrings: string | string[], overrideOptions?: Partial<ExpandIamActionsOptions>): Promise<string[]> {
92
92
  const options = {...defaultOptions, ...overrideOptions}
93
93
 
94
94
  if(!actionStringOrStrings) {
@@ -97,7 +97,12 @@ export function expandIamActions(actionStringOrStrings: string | string[], overr
97
97
  }
98
98
 
99
99
  if(Array.isArray(actionStringOrStrings)) {
100
- let allMatches = actionStringOrStrings.flatMap(actionString => expandIamActions(actionString, options))
100
+ const actionLists = await Promise.all(actionStringOrStrings.map(async (actionString) => {
101
+ return expandIamActions(actionString, options);
102
+ }))
103
+
104
+ let allMatches = actionLists.flat()
105
+
101
106
  if(options.distinct) {
102
107
  const aSet = new Set<string>()
103
108
  allMatches = allMatches.filter((value) => {
@@ -116,12 +121,16 @@ export function expandIamActions(actionStringOrStrings: string | string[], overr
116
121
 
117
122
  const actionString = actionStringOrStrings.trim()
118
123
 
119
- if(actionString.match(allAsteriksPattern)) {
120
- if(options.expandAsterik) {
124
+ if(actionString.match(allAsterisksPattern)) {
125
+ if(options.expandAsterisk) {
121
126
  //If that's really what you want...
122
- return iamServiceKeys().flatMap(
123
- service => iamActionsForService(service).map(action => `${service}:${action}`)
124
- )
127
+ const allActions = []
128
+ const serviceKeys = await iamServiceKeys()
129
+ for await (const service of serviceKeys) {
130
+ const serviceActions = await iamActionsForService(service)
131
+ allActions.push(...serviceActions.map(action => `${service}:${action}`))
132
+ }
133
+ return allActions
125
134
  }
126
135
  return ['*']
127
136
  }
@@ -142,24 +151,26 @@ export function expandIamActions(actionStringOrStrings: string | string[], overr
142
151
  }
143
152
 
144
153
  const [service, wildcardActions] = parts.map(part => part.toLowerCase())
145
- if(!iamServiceExists(service)) {
154
+ if(!await iamServiceExists(service)) {
146
155
  if(options.errorOnMissingService) {
147
156
  throw new Error(`Service not found: ${service}`)
148
157
  }
149
158
  return []
150
159
  }
151
160
 
152
- if(wildcardActions.match(allAsteriksPattern)) {
153
- if(options.expandServiceAsterik) {
154
- return iamActionsForService(service).map(action => `${service}:${action}`)
161
+ if(wildcardActions.match(allAsterisksPattern)) {
162
+ if(options.expandServiceAsterisk) {
163
+ const actionsForService = await iamActionsForService(service)
164
+ return actionsForService.map(action => `${service}:${action}`)
155
165
  }
156
166
  return [`${service}:*`]
157
167
  }
158
168
 
159
169
  if(!actionString.includes('*')) {
160
- const actionExists = iamActionExists(service, wildcardActions)
170
+ const actionExists = await iamActionExists(service, wildcardActions)
161
171
  if(actionExists) {
162
- return [service + ":" + iamActionDetails(service, wildcardActions).name]
172
+ const details = await iamActionDetails(service, wildcardActions)
173
+ return [service + ":" + details.name]
163
174
  }
164
175
 
165
176
  if(options.invalidActionBehavior === InvalidActionBehavior.Remove) {
@@ -174,7 +185,7 @@ export function expandIamActions(actionStringOrStrings: string | string[], overr
174
185
  }
175
186
  }
176
187
 
177
- const allActions = iamActionsForService(service)
188
+ const allActions = await iamActionsForService(service)
178
189
  const pattern = "^" + wildcardActions.replace(/\*/g, '.*?') + "$"
179
190
  const regex = new RegExp(pattern, 'i')
180
191
  const matchingActions = allActions.filter(action => regex.test(action)).map(action => `${service}:${action}`)
@@ -0,0 +1,107 @@
1
+ import { beforeEach } from "node:test";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { expandIamActions } from "./expand.js";
4
+ import { expandJsonDocument } from "./expand_file.js";
5
+
6
+ vi.mock('./expand.js')
7
+
8
+ beforeEach(() => {
9
+ vi.resetAllMocks()
10
+ })
11
+
12
+ describe('expand_file', () => {
13
+ describe('expandJsonDocument', () => {
14
+ it('should return a document without actions as is', async () => {
15
+ // Given a document without actions
16
+ const document = {
17
+ "key": "value",
18
+ "key2": ["value1", "value2"]
19
+ }
20
+
21
+ // When the document is expanded
22
+ const result = await expandJsonDocument({}, document)
23
+
24
+ // Then the document should be returned as is
25
+ expect(result).toEqual(document)
26
+ })
27
+
28
+ it('should expand a string action', async () => {
29
+ // Given a document with an action
30
+ const document = {
31
+ a: {
32
+ b: {
33
+ "Action": "s3:Get*"
34
+ }
35
+ }
36
+ }
37
+ vi.mocked(expandIamActions).mockResolvedValue(["s3:GetObject", "s3:GetBucket"])
38
+
39
+ // When the document is expanded
40
+ const result = await expandJsonDocument({}, document)
41
+
42
+ // Then the action should be expanded
43
+ const expected = JSON.parse(JSON.stringify(document))
44
+ expected.a.b.Action = ["s3:GetObject", "s3:GetBucket"]
45
+ expect(result).toEqual(expected)
46
+ })
47
+
48
+ it('should expand an array of string actions', async () => {
49
+ // Given a document with an action
50
+ const document = {
51
+ a: {
52
+ b: {
53
+ "Action": ["s3:Get*", "s3:Put*"]
54
+ }
55
+ }
56
+ }
57
+ vi.mocked(expandIamActions).mockImplementation(async (actions, options) =>{
58
+ expect(options?.distinct).toBe(true)
59
+ return ["s3:GetObject", "s3:GetBucket", "s3:PutObject", "s3:PutBucket"]
60
+ })
61
+
62
+ // When the document is expanded
63
+ const result = await expandJsonDocument({}, document)
64
+
65
+ // Then the action should be expanded
66
+ const expected = JSON.parse(JSON.stringify(document))
67
+ expected.a.b.Action = ["s3:GetObject", "s3:GetBucket", "s3:PutObject", "s3:PutBucket"]
68
+ expect(result).toEqual(expected)
69
+ })
70
+
71
+ it('should not expand an Action if it is an object', async () => {
72
+ // Given a document with an action
73
+ const document = {
74
+ a: {
75
+ b: {
76
+ "Action": {
77
+ "key": "value"
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ // When the document is expanded
84
+ const result = await expandJsonDocument({}, document)
85
+
86
+ // Then the document should be returned as is
87
+ expect(result).toEqual(document)
88
+ })
89
+
90
+ it('should not expand an Action if it is an array of numbers', async () => {
91
+ // Given a document with an action
92
+ const document = {
93
+ a: {
94
+ b: {
95
+ "Action": [1, 2, 3]
96
+ }
97
+ }
98
+ }
99
+
100
+ // When the document is expanded
101
+ const result = await expandJsonDocument({}, document)
102
+
103
+ // Then the document should be returned as is
104
+ expect(result).toEqual(document)
105
+ })
106
+ })
107
+ })
@@ -0,0 +1,39 @@
1
+ import { expandIamActions, ExpandIamActionsOptions } from "./expand.js"
2
+
3
+ /**
4
+ * Takes any JSON document and expands any Action found in the document
5
+ *
6
+ * @param options the options to use when expanding the actions
7
+ * @param document the JSON document to expand
8
+ * @param key the key of the current node in the document
9
+ * @returns the expanded JSON document
10
+ */
11
+ export async function expandJsonDocument(options: Partial<ExpandIamActionsOptions>, document: any, key?: string): Promise<any> {
12
+ if(key === 'Action') {
13
+ if(typeof document === 'string') {
14
+ return await expandIamActions(document, options)
15
+ }
16
+ if(Array.isArray(document) && document.length > 0 && typeof document[0] === 'string') {
17
+ const value = await expandIamActions(document, {...options, distinct: true})
18
+ return value
19
+ }
20
+ }
21
+
22
+ if(Array.isArray(document)) {
23
+ return Promise.all(document.map(async (item) => {
24
+ return expandJsonDocument(options, item)
25
+ }))
26
+ }
27
+
28
+ if(typeof document === 'object') {
29
+ const keys = Object.keys(document)
30
+ const newObject: any = {}
31
+ for(const key of keys) {
32
+ const value = document[key]
33
+ newObject[key] = await expandJsonDocument(options, value, key)
34
+ }
35
+ return newObject
36
+ }
37
+
38
+ return document
39
+ }