@cloud-copilot/iam-utils 0.0.1
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/.github/workflows/guarddog.yml +31 -0
- package/.github/workflows/pr-checks.yml +86 -0
- package/.github/workflows/release.yml +33 -0
- package/.github/workflows/update-dependencies.yml +16 -0
- package/LICENSE.txt +661 -0
- package/package.json +98 -0
- package/postbuild.sh +11 -0
- package/src/arn.test.ts +126 -0
- package/src/arn.ts +81 -0
- package/src/index.ts +7 -0
- package/src/principals.test.ts +183 -0
- package/src/principals.ts +62 -0
- package/tsconfig.cjs.json +11 -0
- package/tsconfig.esm.json +14 -0
- package/tsconfig.json +26 -0
package/package.json
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cloud-copilot/iam-utils",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"aws",
|
|
7
|
+
"iam",
|
|
8
|
+
"utils"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/cloud-copilot/iam-utils#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/cloud-copilot/iam-utils/issues"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+ssh://git@github.com/cloud-copilot/iam-utils.git"
|
|
17
|
+
},
|
|
18
|
+
"license": "AGPL-3.0-or-later",
|
|
19
|
+
"author": "David Kerber <dave@cloudcopilot.io>",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "npx tsc -p tsconfig.cjs.json && npx tsc -p tsconfig.esm.json && ./postbuild.sh",
|
|
22
|
+
"clean": "rm -rf dist",
|
|
23
|
+
"test": "npx vitest --run --coverage",
|
|
24
|
+
"release": "npm install && npm run clean && npm run build && npm test && npm run format-check && npm publish",
|
|
25
|
+
"format": "npx prettier --write src/",
|
|
26
|
+
"format-check": "npx prettier --check src/"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@cloud-copilot/prettier-config": "^0.1.1",
|
|
30
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
31
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
32
|
+
"@semantic-release/git": "^10.0.1",
|
|
33
|
+
"@semantic-release/github": "^11.0.1",
|
|
34
|
+
"@semantic-release/npm": "^12.0.1",
|
|
35
|
+
"@semantic-release/release-notes-generator": "^14.0.3",
|
|
36
|
+
"@types/node": "^22.5.0",
|
|
37
|
+
"@vitest/coverage-v8": "^3.0.7",
|
|
38
|
+
"semantic-release": "^24.2.1",
|
|
39
|
+
"typescript": "^5.5.4",
|
|
40
|
+
"vitest": "^3.0.7"
|
|
41
|
+
},
|
|
42
|
+
"prettier": "@cloud-copilot/prettier-config",
|
|
43
|
+
"release": {
|
|
44
|
+
"branches": [
|
|
45
|
+
"main"
|
|
46
|
+
],
|
|
47
|
+
"plugins": [
|
|
48
|
+
[
|
|
49
|
+
"@semantic-release/commit-analyzer",
|
|
50
|
+
{
|
|
51
|
+
"releaseRules": [
|
|
52
|
+
{
|
|
53
|
+
"type": "feat",
|
|
54
|
+
"release": "patch"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"type": "fix",
|
|
58
|
+
"release": "patch"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"breaking": true,
|
|
62
|
+
"release": "patch"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"type": "*",
|
|
66
|
+
"release": "patch"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"@semantic-release/release-notes-generator",
|
|
72
|
+
"@semantic-release/changelog",
|
|
73
|
+
[
|
|
74
|
+
"@semantic-release/npm",
|
|
75
|
+
{
|
|
76
|
+
"npmPublish": true
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
[
|
|
80
|
+
"@semantic-release/git",
|
|
81
|
+
{
|
|
82
|
+
"assets": [
|
|
83
|
+
"package.json",
|
|
84
|
+
"package-lock.json",
|
|
85
|
+
"CHANGELOG.md"
|
|
86
|
+
],
|
|
87
|
+
"message": "chore(release): ${nextRelease.version} [skip ci]"
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
[
|
|
91
|
+
"@semantic-release/github",
|
|
92
|
+
{
|
|
93
|
+
"assets": []
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
}
|
package/postbuild.sh
ADDED
package/src/arn.test.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { ArnParts, splitArnParts } from './arn.js'
|
|
3
|
+
|
|
4
|
+
const splitArnPartsTests: {
|
|
5
|
+
name: string
|
|
6
|
+
arn: string
|
|
7
|
+
expected: ArnParts
|
|
8
|
+
only?: boolean
|
|
9
|
+
}[] = [
|
|
10
|
+
{
|
|
11
|
+
name: 'should split a full ARN',
|
|
12
|
+
arn: 'arn:aws:iam::123456789012:user/Development/user1',
|
|
13
|
+
expected: {
|
|
14
|
+
partition: 'aws',
|
|
15
|
+
service: 'iam',
|
|
16
|
+
region: '',
|
|
17
|
+
accountId: '123456789012',
|
|
18
|
+
resource: 'user/Development/user1',
|
|
19
|
+
resourceType: 'user',
|
|
20
|
+
resourcePath: 'Development/user1'
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'should split an S3 bucket ARN',
|
|
25
|
+
arn: 'arn:aws:s3:::my_corporate_bucket',
|
|
26
|
+
expected: {
|
|
27
|
+
partition: 'aws',
|
|
28
|
+
service: 's3',
|
|
29
|
+
region: '',
|
|
30
|
+
accountId: '',
|
|
31
|
+
resource: 'my_corporate_bucket',
|
|
32
|
+
resourceType: '',
|
|
33
|
+
resourcePath: 'my_corporate_bucket'
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'should split an S3 object ARN',
|
|
38
|
+
arn: 'arn:aws:s3:::my_corporate_bucket/my_corporate_object.txt',
|
|
39
|
+
expected: {
|
|
40
|
+
partition: 'aws',
|
|
41
|
+
service: 's3',
|
|
42
|
+
region: '',
|
|
43
|
+
accountId: '',
|
|
44
|
+
resource: 'my_corporate_bucket/my_corporate_object.txt',
|
|
45
|
+
resourceType: '',
|
|
46
|
+
resourcePath: 'my_corporate_bucket/my_corporate_object.txt'
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'should split an SNS topic ARN',
|
|
51
|
+
arn: 'arn:aws:sns:us-east-1:123456789012:MyTopic',
|
|
52
|
+
expected: {
|
|
53
|
+
partition: 'aws',
|
|
54
|
+
service: 'sns',
|
|
55
|
+
region: 'us-east-1',
|
|
56
|
+
accountId: '123456789012',
|
|
57
|
+
resource: 'MyTopic',
|
|
58
|
+
resourceType: '',
|
|
59
|
+
resourcePath: 'MyTopic'
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'should split an SQS queue ARN',
|
|
64
|
+
arn: 'arn:aws:sqs:us-east-1:123456789012:MyQueue',
|
|
65
|
+
expected: {
|
|
66
|
+
partition: 'aws',
|
|
67
|
+
service: 'sqs',
|
|
68
|
+
region: 'us-east-1',
|
|
69
|
+
accountId: '123456789012',
|
|
70
|
+
resource: 'MyQueue',
|
|
71
|
+
resourceType: '',
|
|
72
|
+
resourcePath: 'MyQueue'
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'should split a Lambda function ARN',
|
|
77
|
+
arn: 'arn:aws:lambda:us-west-2:123456789012:function:my-function',
|
|
78
|
+
expected: {
|
|
79
|
+
partition: 'aws',
|
|
80
|
+
service: 'lambda',
|
|
81
|
+
region: 'us-west-2',
|
|
82
|
+
accountId: '123456789012',
|
|
83
|
+
resource: 'function:my-function',
|
|
84
|
+
resourceType: 'function',
|
|
85
|
+
resourcePath: 'my-function'
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'rest api ARN',
|
|
90
|
+
arn: 'arn:aws:apigateway:us-east-1::/restapis/1234567890',
|
|
91
|
+
expected: {
|
|
92
|
+
partition: 'aws',
|
|
93
|
+
service: 'apigateway',
|
|
94
|
+
region: 'us-east-1',
|
|
95
|
+
accountId: '',
|
|
96
|
+
resource: '/restapis/1234567890',
|
|
97
|
+
resourceType: 'restapis',
|
|
98
|
+
resourcePath: '1234567890'
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'should split a glue root catalog ARN',
|
|
103
|
+
arn: 'arn:aws:glue:us-east-1:111111111111:catalog',
|
|
104
|
+
expected: {
|
|
105
|
+
partition: 'aws',
|
|
106
|
+
service: 'glue',
|
|
107
|
+
region: 'us-east-1',
|
|
108
|
+
accountId: '111111111111',
|
|
109
|
+
resource: 'catalog',
|
|
110
|
+
resourceType: 'catalog',
|
|
111
|
+
resourcePath: ''
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
describe('splitArnParts', () => {
|
|
117
|
+
for (const test of splitArnPartsTests) {
|
|
118
|
+
const { name, arn, expected, only } = test
|
|
119
|
+
const testFn = only ? it.only : it
|
|
120
|
+
|
|
121
|
+
testFn(name, () => {
|
|
122
|
+
const result = splitArnParts(arn)
|
|
123
|
+
expect(result).toEqual(expected)
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
})
|
package/src/arn.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export interface ArnParts {
|
|
2
|
+
partition: string | undefined
|
|
3
|
+
service: string | undefined
|
|
4
|
+
region: string | undefined
|
|
5
|
+
accountId: string | undefined
|
|
6
|
+
resource: string | undefined
|
|
7
|
+
resourceType: string | undefined
|
|
8
|
+
resourcePath: string | undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Split an ARN into its parts
|
|
13
|
+
*
|
|
14
|
+
* @param arn the arn to split
|
|
15
|
+
* @returns the parts of the ARN
|
|
16
|
+
*/
|
|
17
|
+
export function splitArnParts(arn: string): ArnParts {
|
|
18
|
+
const parts = arn.split(':')
|
|
19
|
+
const partition = parts.at(1)
|
|
20
|
+
const service = parts.at(2)!
|
|
21
|
+
const region = parts.at(3)!
|
|
22
|
+
const accountId = parts.at(4)!
|
|
23
|
+
const resource = parts.slice(5).join(':')
|
|
24
|
+
const [resourceType, resourcePath] = getResourceSegments(service, accountId, region, resource)
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
partition,
|
|
28
|
+
service,
|
|
29
|
+
region,
|
|
30
|
+
accountId,
|
|
31
|
+
resource,
|
|
32
|
+
resourceType,
|
|
33
|
+
resourcePath
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get the product/id segments of the resource portion of an ARN.
|
|
39
|
+
* The first segment is the product segment and the second segment is the resource id segment.
|
|
40
|
+
* This could be split by a colon or a slash, so it checks for both. It also checks for S3 buckets/objects.
|
|
41
|
+
*
|
|
42
|
+
* @param resource The resource to get the resource segments. Must be an ARN resource.
|
|
43
|
+
* @returns a tuple with the first segment being the product segment (without the separator) and the second segment being the resource id.
|
|
44
|
+
*/
|
|
45
|
+
export function getResourceSegments(
|
|
46
|
+
service: string,
|
|
47
|
+
accountId: string,
|
|
48
|
+
region: string,
|
|
49
|
+
resourceString: string
|
|
50
|
+
): [string, string] {
|
|
51
|
+
// This is terrible, and I hate it
|
|
52
|
+
if (
|
|
53
|
+
(service === 's3' && accountId === '' && region === '') ||
|
|
54
|
+
service === 'sns' ||
|
|
55
|
+
service === 'sqs'
|
|
56
|
+
) {
|
|
57
|
+
return ['', resourceString]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (resourceString.startsWith('/')) {
|
|
61
|
+
resourceString = resourceString.slice(1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const slashIndex = resourceString.indexOf('/')
|
|
65
|
+
const colonIndex = resourceString.indexOf(':')
|
|
66
|
+
|
|
67
|
+
let splitIndex = slashIndex
|
|
68
|
+
if (slashIndex != -1 && colonIndex != -1) {
|
|
69
|
+
splitIndex = Math.min(slashIndex, colonIndex) + 1
|
|
70
|
+
} else if (slashIndex == -1 && colonIndex == -1) {
|
|
71
|
+
splitIndex = resourceString.length + 1
|
|
72
|
+
} else if (colonIndex == -1) {
|
|
73
|
+
splitIndex = slashIndex + 1
|
|
74
|
+
} else if (slashIndex == -1) {
|
|
75
|
+
splitIndex = colonIndex + 1
|
|
76
|
+
} else {
|
|
77
|
+
throw new Error(`Unable to split resource ${resourceString}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return [resourceString.slice(0, splitIndex - 1), resourceString.slice(splitIndex)]
|
|
81
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
convertAssumedRoleArnToRoleArn,
|
|
4
|
+
convertRoleArnToAssumedRoleArn,
|
|
5
|
+
isAssumedRoleArn,
|
|
6
|
+
isFederatedUserArn,
|
|
7
|
+
isIamUserArn
|
|
8
|
+
} from './principals.js'
|
|
9
|
+
|
|
10
|
+
describe('convertAssumedRoleArnToRoleArn', () => {
|
|
11
|
+
it('should return the role ARN from an assumed role ARN', () => {
|
|
12
|
+
//Given an assumed role ARN
|
|
13
|
+
const assumedRoleArn = 'arn:aws:sts::123456789012:assumed-role/role-name/session-name'
|
|
14
|
+
|
|
15
|
+
//When we get the role ARN from the assumed role ARN
|
|
16
|
+
const result = convertAssumedRoleArnToRoleArn(assumedRoleArn)
|
|
17
|
+
|
|
18
|
+
//Then it should return the role ARN
|
|
19
|
+
expect(result).toBe('arn:aws:iam::123456789012:role/role-name')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should return the role ARN from an assumed role ARN with a path', () => {
|
|
23
|
+
//Given an assumed role ARN
|
|
24
|
+
const assumedRoleArn = 'arn:aws:sts::123456789012:assumed-role/admin/global-admin/session-name'
|
|
25
|
+
|
|
26
|
+
//When we get the role ARN from the assumed role ARN
|
|
27
|
+
const result = convertAssumedRoleArnToRoleArn(assumedRoleArn)
|
|
28
|
+
|
|
29
|
+
//Then it should return the role ARN
|
|
30
|
+
expect(result).toBe('arn:aws:iam::123456789012:role/admin/global-admin')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should work in a different partition', () => {
|
|
34
|
+
//Given an assumed role ARN with a different partition
|
|
35
|
+
const assumedRoleArn = 'arn:aws-cn:sts::123456789012:assumed-role/role-name/session-name'
|
|
36
|
+
|
|
37
|
+
//When we get the role ARN from the assumed role ARN
|
|
38
|
+
const result = convertAssumedRoleArnToRoleArn(assumedRoleArn)
|
|
39
|
+
|
|
40
|
+
//Then it should return the role ARN
|
|
41
|
+
expect(result).toBe('arn:aws-cn:iam::123456789012:role/role-name')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('convertRoleArnToAssumedRoleArn', () => {
|
|
46
|
+
it('should return the assumed role ARN from a role ARN', () => {
|
|
47
|
+
//Given a role ARN
|
|
48
|
+
const roleArn = 'arn:aws:iam::123456789012:role/role-name'
|
|
49
|
+
|
|
50
|
+
//When we get the assumed role ARN from the role ARN
|
|
51
|
+
const result = convertRoleArnToAssumedRoleArn(roleArn, 'session-name')
|
|
52
|
+
|
|
53
|
+
//Then it should return the assumed role ARN
|
|
54
|
+
expect(result).toBe('arn:aws:sts::123456789012:assumed-role/role-name/session-name')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should return the assumed role ARN from a role ARN with a path', () => {
|
|
58
|
+
//Given a role ARN
|
|
59
|
+
const roleArn = 'arn:aws:iam::123456789012:role/admin/global-admin'
|
|
60
|
+
|
|
61
|
+
//When we get the assumed role ARN from the role ARN
|
|
62
|
+
const result = convertRoleArnToAssumedRoleArn(roleArn, 'session-name')
|
|
63
|
+
|
|
64
|
+
//Then it should return the assumed role ARN
|
|
65
|
+
expect(result).toBe('arn:aws:sts::123456789012:assumed-role/admin/global-admin/session-name')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should work with a different partition', () => {
|
|
69
|
+
//Given a role ARN with a different partition
|
|
70
|
+
const roleArn = 'arn:aws-cn:iam::123456789012:role/role-name'
|
|
71
|
+
|
|
72
|
+
//When we get the assumed role ARN from the role ARN
|
|
73
|
+
const result = convertRoleArnToAssumedRoleArn(roleArn, 'session-name')
|
|
74
|
+
|
|
75
|
+
//Then it should return the assumed role ARN
|
|
76
|
+
expect(result).toBe('arn:aws-cn:sts::123456789012:assumed-role/role-name/session-name')
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('isAssumedRoleArn', () => {
|
|
81
|
+
it('should return true for assumed role ARN', () => {
|
|
82
|
+
//Given an assumed role ARN
|
|
83
|
+
const assumedRoleArn = 'arn:aws:sts::123456789012:assumed-role/role-name/session-name'
|
|
84
|
+
|
|
85
|
+
//When we check if it is an assumed role ARN
|
|
86
|
+
const result = isAssumedRoleArn(assumedRoleArn)
|
|
87
|
+
|
|
88
|
+
//Then it should return true
|
|
89
|
+
expect(result).toBe(true)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should return false for non-assumed role ARN', () => {
|
|
93
|
+
//Given a non-assumed role ARN
|
|
94
|
+
const userArn = 'arn:aws:iam::123456789012:user/user-name'
|
|
95
|
+
|
|
96
|
+
//When we check if it is an assumed role ARN
|
|
97
|
+
const result = isAssumedRoleArn(userArn)
|
|
98
|
+
|
|
99
|
+
//Then it should return false
|
|
100
|
+
expect(result).toBe(false)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should work for a different partition', () => {
|
|
104
|
+
//Given an assumed role ARN with a different partition
|
|
105
|
+
const assumedRoleArn = 'arn:aws-cn:sts::123456789012:assumed-role/role-name/session-name'
|
|
106
|
+
|
|
107
|
+
//When we check if it is an assumed role ARN
|
|
108
|
+
const result = isAssumedRoleArn(assumedRoleArn)
|
|
109
|
+
|
|
110
|
+
//Then it should return true
|
|
111
|
+
expect(result).toBe(true)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('isIamUserArn', () => {
|
|
116
|
+
it('should return true for IAM user ARN', () => {
|
|
117
|
+
//Given an IAM user ARN
|
|
118
|
+
const userArn = 'arn:aws:iam::123456789012:user/user-name'
|
|
119
|
+
|
|
120
|
+
//When we check if it is an IAM user ARN
|
|
121
|
+
const result = isIamUserArn(userArn)
|
|
122
|
+
|
|
123
|
+
//Then it should return true
|
|
124
|
+
expect(result).toBe(true)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should return false for non-IAM user ARN', () => {
|
|
128
|
+
//Given a non-IAM user ARN
|
|
129
|
+
const roleArn = 'arn:aws:sts::123456789012:assumed-role/role-name/session-name'
|
|
130
|
+
|
|
131
|
+
//When we check if it is an IAM user ARN
|
|
132
|
+
const result = isIamUserArn(roleArn)
|
|
133
|
+
|
|
134
|
+
//Then it should return false
|
|
135
|
+
expect(result).toBe(false)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('should work for a different partition', () => {
|
|
139
|
+
//Given an IAM user ARN with a different partition
|
|
140
|
+
const userArn = 'arn:aws-cn:iam::123456789012:user/user-name'
|
|
141
|
+
|
|
142
|
+
//When we check if it is an IAM user ARN
|
|
143
|
+
const result = isIamUserArn(userArn)
|
|
144
|
+
|
|
145
|
+
//Then it should return true
|
|
146
|
+
expect(result).toBe(true)
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('isFederatedUserArn', () => {
|
|
151
|
+
it('should return true for federated user ARN', () => {
|
|
152
|
+
//Given a federated user ARN
|
|
153
|
+
const federatedUserArn = 'arn:aws:sts::123456789012:federated-user/user-name'
|
|
154
|
+
|
|
155
|
+
//When we check if it is a federated user ARN
|
|
156
|
+
const result = isFederatedUserArn(federatedUserArn)
|
|
157
|
+
|
|
158
|
+
//Then it should return true
|
|
159
|
+
expect(result).toBe(true)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should return false for non-federated user ARN', () => {
|
|
163
|
+
//Given a non-federated user ARN
|
|
164
|
+
const roleArn = 'arn:aws:sts::123456789012:assumed-role/role-name/session-name'
|
|
165
|
+
|
|
166
|
+
//When we check if it is a federated user ARN
|
|
167
|
+
const result = isFederatedUserArn(roleArn)
|
|
168
|
+
|
|
169
|
+
//Then it should return false
|
|
170
|
+
expect(result).toBe(false)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('should work for a different partition', () => {
|
|
174
|
+
//Given a federated user ARN with a different partition
|
|
175
|
+
const federatedUserArn = 'arn:aws-cn:sts::123456789012:federated-user/user-name'
|
|
176
|
+
|
|
177
|
+
//When we check if it is a federated user ARN
|
|
178
|
+
const result = isFederatedUserArn(federatedUserArn)
|
|
179
|
+
|
|
180
|
+
//Then it should return true
|
|
181
|
+
expect(result).toBe(true)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { splitArnParts } from './arn.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Transform an assumed role session ARN into a role ARN
|
|
5
|
+
*
|
|
6
|
+
* @param assumedRoleArn the assumed role session ARN
|
|
7
|
+
* @returns the role ARN for the assumed role session
|
|
8
|
+
*/
|
|
9
|
+
export function convertAssumedRoleArnToRoleArn(assumedRoleArn: string): string {
|
|
10
|
+
const arnParts = splitArnParts(assumedRoleArn)
|
|
11
|
+
const rolePathAndName = arnParts.resourcePath?.split('/').slice(0, -1).join('/')
|
|
12
|
+
return `arn:${arnParts.partition}:iam::${arnParts.accountId}:role/${rolePathAndName}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create an assumed role ARN from a role ARN and a session name
|
|
17
|
+
*
|
|
18
|
+
* @param roleArn the role ARN to create an assumed role ARN from
|
|
19
|
+
* @param sessionName the session name to use
|
|
20
|
+
* @returns the assumed role ARN
|
|
21
|
+
*/
|
|
22
|
+
export function convertRoleArnToAssumedRoleArn(roleArn: string, sessionName: string): string {
|
|
23
|
+
const arnParts = splitArnParts(roleArn)
|
|
24
|
+
const rolePathAndName = arnParts.resourcePath
|
|
25
|
+
return `arn:${arnParts.partition}:sts::${arnParts.accountId}:assumed-role/${rolePathAndName}/${sessionName}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const assumedRoleArnRegex = /^arn:[a-zA-Z\-]+:sts::\d{12}:assumed-role\/.*$/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Tests if a principal string is an assumed role ARN
|
|
32
|
+
*
|
|
33
|
+
* @param principal the principal string to test
|
|
34
|
+
* @returns true if the principal is an assumed role ARN, false otherwise
|
|
35
|
+
*/
|
|
36
|
+
export function isAssumedRoleArn(principal: string): boolean {
|
|
37
|
+
return assumedRoleArnRegex.test(principal)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const userArnRegex = /^arn:[a-zA-Z\-]+:iam::\d{12}:user\/.*$/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Test if a principal string is an IAM user ARN
|
|
44
|
+
*
|
|
45
|
+
* @param principal the principal string to test
|
|
46
|
+
* @returns true if the principal is an IAM user ARN, false otherwise
|
|
47
|
+
*/
|
|
48
|
+
export function isIamUserArn(principal: string): boolean {
|
|
49
|
+
return userArnRegex.test(principal)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const federatedUserArnRegex = /^arn:[a-zA-Z\-]+:sts::\d{12}:federated-user\/.*$/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Test if a principal string is a federated user ARN
|
|
56
|
+
*
|
|
57
|
+
* @param principal the principal string to test
|
|
58
|
+
* @returns true if the principal is a federated user ARN, false otherwise
|
|
59
|
+
*/
|
|
60
|
+
export function isFederatedUserArn(principal: string): boolean {
|
|
61
|
+
return federatedUserArnRegex.test(principal)
|
|
62
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "commonjs",
|
|
4
|
+
"target": "es2022",
|
|
5
|
+
"outDir": "dist",
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
"sourceMap": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"lib": ["es2023", "DOM"],
|
|
12
|
+
"noUnusedLocals": false,
|
|
13
|
+
"noUnusedParameters": false,
|
|
14
|
+
"noImplicitReturns": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": false,
|
|
16
|
+
"experimentalDecorators": true,
|
|
17
|
+
"emitDecoratorMetadata": true,
|
|
18
|
+
"esModuleInterop": false,
|
|
19
|
+
"forceConsistentCasingInFileNames": true,
|
|
20
|
+
"paths": {
|
|
21
|
+
// workaround for: https://github.com/vitest-dev/vitest/issues/4567
|
|
22
|
+
"rollup/parseAst": ["./node_modules/rollup/dist/parseAst"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"exclude": ["tests", "test", "dist", "bin", "**/bin", "**/dist", "node_modules", "cdk.out"],
|
|
26
|
+
}
|