1+ #!/usr/bin/env python3
2+ """
3+ AWS IAM Role Setup for GitHub Actions - NPM Publishing
4+ Creates the necessary IAM role and OIDC provider for GitHub Actions to publish NPM packages.
5+ """
6+
7+ import boto3
8+ import json
9+ import sys
10+ from botocore .exceptions import ClientError
11+
12+
13+ def get_github_repo_info ():
14+ """Get GitHub repository information from git remote."""
15+ print ("📋 Detecting GitHub Repository Information..." )
16+
17+ try :
18+ import subprocess
19+
20+ # Get remote URL
21+ result = subprocess .run (['git' , 'remote' , 'get-url' , 'origin' ],
22+ capture_output = True , text = True , check = True )
23+ remote_url = result .stdout .strip ()
24+
25+ # Parse GitHub URL
26+ if 'github.com' in remote_url :
27+ # Handle both SSH and HTTPS URLs
28+ if remote_url .startswith ('git@github.com:' ):
29+ # SSH: git@github.com:owner/repo.git
30+ repo_path = remote_url .replace ('git@github.com:' , '' ).replace ('.git' , '' )
31+ elif 'github.com/' in remote_url :
32+ # HTTPS: https://github.com/owner/repo.git
33+ repo_path = remote_url .split ('github.com/' )[- 1 ].replace ('.git' , '' )
34+ else :
35+ raise ValueError ("Cannot parse GitHub URL format" )
36+
37+ github_org , github_repo = repo_path .split ('/' , 1 )
38+
39+ print (f"✅ Detected GitHub repository: { github_org } /{ github_repo } " )
40+ return github_org , github_repo
41+ else :
42+ raise ValueError ("Remote is not a GitHub repository" )
43+
44+ except Exception as e :
45+ print (f"❌ Could not auto-detect GitHub repository: { e } " )
46+ print ("Please ensure you're in a git repository with GitHub remote" )
47+ sys .exit (1 )
48+
49+
50+ def create_oidc_provider (iam_client ):
51+ """Create GitHub OIDC identity provider if it doesn't exist."""
52+ print ("\n 🔐 Setting up GitHub OIDC Provider..." )
53+
54+ github_oidc_url = "https://token.actions.githubusercontent.com"
55+ github_thumbprint = "6938fd4d98bab03faadb97b34396831e3780aea1" # GitHub's thumbprint
56+
57+ try :
58+ # Check if OIDC provider already exists
59+ response = iam_client .list_open_id_connect_providers ()
60+ for provider in response ['OpenIDConnectProviderList' ]:
61+ if github_oidc_url in provider ['Arn' ]:
62+ print (f"✅ GitHub OIDC provider already exists: { provider ['Arn' ]} " )
63+ return provider ['Arn' ]
64+
65+ # Create OIDC provider
66+ response = iam_client .create_open_id_connect_provider (
67+ Url = github_oidc_url ,
68+ ThumbprintList = [github_thumbprint ],
69+ ClientIDList = ['sts.amazonaws.com' ]
70+ )
71+
72+ oidc_arn = response ['OpenIDConnectProviderArn' ]
73+ print (f"✅ Created GitHub OIDC provider: { oidc_arn } " )
74+ return oidc_arn
75+
76+ except ClientError as e :
77+ if 'EntityAlreadyExists' in str (e ):
78+ print ("✅ GitHub OIDC provider already exists" )
79+ # Get the ARN
80+ response = iam_client .list_open_id_connect_providers ()
81+ for provider in response ['OpenIDConnectProviderList' ]:
82+ if github_oidc_url in provider ['Arn' ]:
83+ return provider ['Arn' ]
84+ else :
85+ print (f"❌ Error creating OIDC provider: { e } " )
86+ sys .exit (1 )
87+
88+
89+ def create_trust_policy (account_id , github_org , github_repo ):
90+ """Create trust policy for GitHub Actions."""
91+ return {
92+ "Version" : "2012-10-17" ,
93+ "Statement" : [
94+ {
95+ "Effect" : "Allow" ,
96+ "Principal" : {
97+ "Federated" : f"arn:aws:iam::{ account_id } :oidc-provider/token.actions.githubusercontent.com"
98+ },
99+ "Action" : "sts:AssumeRoleWithWebIdentity" ,
100+ "Condition" : {
101+ "StringEquals" : {
102+ "token.actions.githubusercontent.com:aud" : "sts.amazonaws.com"
103+ },
104+ "StringLike" : {
105+ "token.actions.githubusercontent.com:sub" : [
106+ f"repo:{ github_org } /{ github_repo } :*"
107+ ]
108+ }
109+ }
110+ }
111+ ]
112+ }
113+
114+
115+ def create_npm_publish_policy ():
116+ """Create IAM policy for NPM publishing permissions."""
117+ return {
118+ "Version" : "2012-10-17" ,
119+ "Statement" : [
120+ {
121+ "Sid" : "SSMParameterStoreAccess" ,
122+ "Effect" : "Allow" ,
123+ "Action" : [
124+ # SSM Parameter Store permissions for NPM token management
125+ "ssm:PutParameter" ,
126+ "ssm:GetParameter" ,
127+ "ssm:GetParameters" ,
128+ "ssm:DeleteParameter" ,
129+ "ssm:DescribeParameters" ,
130+ "ssm:GetParameterHistory" ,
131+ "ssm:GetParametersByPath" ,
132+ "ssm:AddTagsToResource" ,
133+ "ssm:RemoveTagsFromResource" ,
134+ "ssm:ListTagsForResource"
135+ ],
136+ "Resource" : [
137+ # Specific to NPM token parameters
138+ "arn:aws:ssm:*:*:parameter/npm/tokens/*" ,
139+ "arn:aws:ssm:*:*:parameter/github/tokens/*" ,
140+ "arn:aws:ssm:*:*:parameter/ci/tokens/*"
141+ ]
142+ },
143+ {
144+ "Sid" : "SSMDescribeAccess" ,
145+ "Effect" : "Allow" ,
146+ "Action" : [
147+ "ssm:DescribeParameters"
148+ ],
149+ "Resource" : "*"
150+ },
151+ {
152+ "Sid" : "KMSKeyAccess" ,
153+ "Effect" : "Allow" ,
154+ "Action" : [
155+ # KMS permissions for SecureString parameters
156+ "kms:Decrypt" ,
157+ "kms:Encrypt" ,
158+ "kms:GenerateDataKey" ,
159+ "kms:DescribeKey"
160+ ],
161+ "Resource" : "*" ,
162+ "Condition" : {
163+ "StringLike" : {
164+ "kms:ViaService" : [
165+ "ssm.*.amazonaws.com"
166+ ]
167+ }
168+ }
169+ },
170+ {
171+ "Sid" : "CloudWatchLogsAccess" ,
172+ "Effect" : "Allow" ,
173+ "Action" : [
174+ # CloudWatch Logs for GitHub Actions debugging
175+ "logs:CreateLogGroup" ,
176+ "logs:CreateLogStream" ,
177+ "logs:PutLogEvents" ,
178+ "logs:DescribeLogGroups" ,
179+ "logs:DescribeLogStreams"
180+ ],
181+ "Resource" : [
182+ "arn:aws:logs:*:*:log-group:/github-actions/*" ,
183+ "arn:aws:logs:*:*:log-group:/aws/lambda/github-actions-*"
184+ ]
185+ },
186+ {
187+ "Sid" : "STSAssumeRole" ,
188+ "Effect" : "Allow" ,
189+ "Action" : [
190+ # STS permissions for role assumption
191+ "sts:GetCallerIdentity" ,
192+ "sts:GetSessionToken"
193+ ],
194+ "Resource" : "*"
195+ }
196+ ]
197+ }
198+
199+
200+ def create_npm_role (iam_client , role_name , github_org , github_repo , account_id ):
201+ """Create the NPM publishing role for GitHub Actions."""
202+ print (f"\n 🔧 Creating NPM publishing role: { role_name } " )
203+
204+ trust_policy = create_trust_policy (account_id , github_org , github_repo )
205+
206+ try :
207+ # Create the role
208+ response = iam_client .create_role (
209+ RoleName = role_name ,
210+ AssumeRolePolicyDocument = json .dumps (trust_policy ),
211+ Description = f"NPM publishing role for GitHub Actions in { github_org } /{ github_repo } " ,
212+ MaxSessionDuration = 3600 # 1 hour
213+ )
214+
215+ role_arn = response ['Role' ]['Arn' ]
216+ print (f"✅ Created role: { role_arn } " )
217+
218+ # Create and attach the NPM publishing policy
219+ policy_name = f"{ role_name } -NPMPublishPolicy"
220+ npm_policy = create_npm_publish_policy ()
221+
222+ iam_client .put_role_policy (
223+ RoleName = role_name ,
224+ PolicyName = policy_name ,
225+ PolicyDocument = json .dumps (npm_policy )
226+ )
227+
228+ print (f"✅ Attached NPM publishing policy: { policy_name } " )
229+ return role_arn
230+
231+ except ClientError as e :
232+ if 'EntityAlreadyExists' in str (e ):
233+ print (f"✅ Role { role_name } already exists" )
234+ response = iam_client .get_role (RoleName = role_name )
235+ role_arn = response ['Role' ]['Arn' ]
236+
237+ # Update the policy even if role exists
238+ policy_name = f"{ role_name } -NPMPublishPolicy"
239+ npm_policy = create_npm_publish_policy ()
240+
241+ iam_client .put_role_policy (
242+ RoleName = role_name ,
243+ PolicyName = policy_name ,
244+ PolicyDocument = json .dumps (npm_policy )
245+ )
246+
247+ print (f"✅ Updated NPM publishing policy: { policy_name } " )
248+ return role_arn
249+ else :
250+ print (f"❌ Error creating role: { e } " )
251+ sys .exit (1 )
252+
253+
254+ def main ():
255+ """Main function to set up GitHub Actions AWS integration for NPM publishing."""
256+ print ("🚀 AWS IAM Role Setup for GitHub Actions - NPM Publishing" )
257+ print ("=" * 50 )
258+
259+ # Initialize AWS clients
260+ try :
261+ sts_client = boto3 .client ('sts' )
262+ iam_client = boto3 .client ('iam' )
263+
264+ # Get current AWS account info
265+ identity = sts_client .get_caller_identity ()
266+ account_id = identity ['Account' ]
267+ current_user = identity ['Arn' ]
268+
269+ print (f"📋 AWS Account: { account_id } " )
270+ print (f"📋 Current User: { current_user } " )
271+
272+ except Exception as e :
273+ print (f"❌ AWS authentication failed: { e } " )
274+ print ("Make sure your AWS credentials are configured (aws configure)" )
275+ sys .exit (1 )
276+
277+ # Get GitHub repository information
278+ github_org , github_repo = get_github_repo_info ()
279+
280+ # Set up role name
281+ role_name = f"github-actions-{ github_org } -{ github_repo } -npm-access"
282+
283+ print (f"\n 📋 Setup Summary:" )
284+ print (f" AWS Account: { account_id } " )
285+ print (f" GitHub Repo: { github_org } /{ github_repo } " )
286+ print (f" Role Name: { role_name } " )
287+
288+ # Auto-proceed (non-interactive)
289+ print ("\n 🚀 Proceeding with automatic setup..." )
290+
291+ # Create OIDC provider
292+ oidc_arn = create_oidc_provider (iam_client )
293+
294+ # Create NPM publishing role
295+ role_arn = create_npm_role (iam_client , role_name , github_org , github_repo , account_id )
296+
297+ print ("\n " + "=" * 50 )
298+ print ("✅ Setup Complete!" )
299+ print ("=" * 50 )
300+
301+ print (f"\n 📋 GitHub Repository Variable to add:" )
302+ print (f" Repository: https://github.com/{ github_org } /{ github_repo } /settings/variables/actions" )
303+ print (f" " )
304+ print (f" Variable Name: AWS_DEPLOYMENT_ROLE" )
305+ print (f" Variable Value: { role_arn } " )
306+ print (f" " )
307+ print (f" Note: This should be a Repository Variable, not a Secret" )
308+
309+ print (f"\n 🔗 Next Steps:" )
310+ print (f" 1. Add the repository variable above to your GitHub repository" )
311+ print (f" 2. Store your NPM token in the workflow when running it" )
312+ print (f" 3. The workflow will save the token to SSM for future use" )
313+
314+ print (f"\n 🔐 Security Notes:" )
315+ print (f" - Role can only be assumed by your specific GitHub repository" )
316+ print (f" - Role has limited permissions for SSM Parameter Store only" )
317+ print (f" - Can only access parameters under /npm/tokens/*, /github/tokens/*, and /ci/tokens/*" )
318+ print (f" - Sessions are limited to 1 hour" )
319+
320+ print (f"\n 📦 NPM Token Storage:" )
321+ print (f" - Tokens will be stored in: /npm/tokens/{ github_org } -{ github_repo } " )
322+ print (f" - Tokens are encrypted using AWS KMS" )
323+ print (f" - Only this GitHub repository can access these tokens" )
324+
325+
326+ if __name__ == '__main__' :
327+ main ()
0 commit comments