@cloud-copilot/iam-expand 0.1.4 → 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/README.md +84 -20
- package/dist/cjs/cli.js +17 -9
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/cli_utils.d.ts +4 -1
- package/dist/cjs/cli_utils.d.ts.map +1 -1
- package/dist/cjs/cli_utils.js +10 -4
- package/dist/cjs/cli_utils.js.map +1 -1
- package/dist/cjs/expand.d.ts +3 -3
- package/dist/cjs/expand.d.ts.map +1 -1
- package/dist/cjs/expand.js +26 -15
- package/dist/cjs/expand.js.map +1 -1
- package/dist/cjs/expand_file.d.ts +11 -0
- package/dist/cjs/expand_file.d.ts.map +1 -0
- package/dist/cjs/expand_file.js +39 -0
- package/dist/cjs/expand_file.js.map +1 -0
- package/dist/esm/cli.js +18 -10
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/cli_utils.d.ts +4 -1
- package/dist/esm/cli_utils.d.ts.map +1 -1
- package/dist/esm/cli_utils.js +9 -3
- package/dist/esm/cli_utils.js.map +1 -1
- package/dist/esm/expand.d.ts +3 -3
- package/dist/esm/expand.d.ts.map +1 -1
- package/dist/esm/expand.js +26 -15
- package/dist/esm/expand.js.map +1 -1
- package/dist/esm/expand_file.d.ts +11 -0
- package/dist/esm/expand_file.d.ts.map +1 -0
- package/dist/esm/expand_file.js +36 -0
- package/dist/esm/expand_file.js.map +1 -0
- package/package.json +4 -3
- package/src/cli.ts +17 -10
- package/src/cli_utils.test.ts +26 -11
- package/src/cli_utils.ts +10 -4
- package/src/expand.test.ts +112 -106
- package/src/expand.ts +30 -19
- package/src/expand_file.test.ts +107 -0
- package/src/expand_file.ts +39 -0
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
120
|
-
if(options.
|
|
124
|
+
if(actionString.match(allAsterisksPattern)) {
|
|
125
|
+
if(options.expandAsterisk) {
|
|
121
126
|
//If that's really what you want...
|
|
122
|
-
|
|
123
|
-
|
|
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(
|
|
153
|
-
if(options.
|
|
154
|
-
|
|
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
|
-
|
|
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
|
+
}
|