cloudmason2 1.5.0 → 1.7.0

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.
@@ -105,6 +105,25 @@ function createLocalMarketplace(){
105
105
  console.log(`Created ${destDir}`);
106
106
  }
107
107
 
108
+ // Trust policy allowing GitHub Actions OIDC assumption from one repo.
109
+ // Shared with update-listing's -repo trust update
110
+ exports.githubTrustPolicy = function(oidcArn, repo){
111
+ return {
112
+ Version: "2012-10-17",
113
+ Statement: [
114
+ {
115
+ Effect: "Allow",
116
+ Principal: { Federated: oidcArn },
117
+ Action: "sts:AssumeRoleWithWebIdentity",
118
+ Condition: {
119
+ StringEquals: { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
120
+ StringLike: { "token.actions.githubusercontent.com:sub": `repo:${repo}:*` }
121
+ }
122
+ }
123
+ ]
124
+ };
125
+ }
126
+
108
127
  // Create a role GitHub Actions can assume via OIDC from the given repo.
109
128
  // Permissions cover update-listing, update-app, launch, and publish - not new-app
110
129
  async function ensureGithubRole(repo, bucket, appPrefix, appName, productId){
@@ -115,34 +134,27 @@ async function ensureGithubRole(repo, bucket, appPrefix, appName, productId){
115
134
 
116
135
  const oidcArn = await ensureOidcProvider(iam, accountId);
117
136
 
137
+ // Reuse the role if it already exists - the permissions policy below is
138
+ // still re-applied, so re-running new-app refreshes an existing role
139
+ let roleArn = null;
118
140
  try {
119
141
  const existing = await iam.send(new GetRoleCommand({ RoleName: roleName }));
120
142
  console.log(`Role ${roleName} already exists (${existing.Role.Arn})`);
121
- return existing.Role.Arn;
143
+ roleArn = existing.Role.Arn;
122
144
  } catch (e){
123
145
  if (!/NoSuchEntity/.test(e)){ throw e }
124
146
  }
125
147
 
126
- // Trust: only GitHub Actions workflows from this repo
127
- const trustPolicy = {
128
- Version: "2012-10-17",
129
- Statement: [
130
- {
131
- Effect: "Allow",
132
- Principal: { Federated: oidcArn },
133
- Action: "sts:AssumeRoleWithWebIdentity",
134
- Condition: {
135
- StringEquals: { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
136
- StringLike: { "token.actions.githubusercontent.com:sub": `repo:${repo}:*` }
137
- }
138
- }
139
- ]
140
- };
141
- const created = await iam.send(new CreateRoleCommand({
142
- RoleName: roleName,
143
- AssumeRolePolicyDocument: JSON.stringify(trustPolicy),
144
- Description: `Mason CI role for ${appName} (github.com/${repo})`
145
- }));
148
+ if (!roleArn){
149
+ // Trust: only GitHub Actions workflows from this repo
150
+ const trustPolicy = exports.githubTrustPolicy(oidcArn, repo);
151
+ const created = await iam.send(new CreateRoleCommand({
152
+ RoleName: roleName,
153
+ AssumeRolePolicyDocument: JSON.stringify(trustPolicy),
154
+ Description: `Mason CI role for ${appName} (github.com/${repo})`
155
+ }));
156
+ roleArn = created.Role.Arn;
157
+ }
146
158
 
147
159
  // Scoped to the app prefix, product id, and app stack names where the
148
160
  // services support it. EC2 build permissions cannot be scoped
@@ -181,6 +193,14 @@ async function ensureGithubRole(repo, bucket, appPrefix, appName, productId){
181
193
  Action: ["cloudformation:CreateStack", "cloudformation:UpdateStack", "cloudformation:DescribeStacks"],
182
194
  Resource: `arn:aws:cloudformation:*:${accountId}:stack/${appName}-*/*`
183
195
  },
196
+ {
197
+ Sid: "ReadTemplateSummary",
198
+ Effect: "Allow",
199
+ // launch calls GetTemplateSummary with a TemplateURL, which IAM
200
+ // evaluates against * rather than a stack ARN
201
+ Action: "cloudformation:GetTemplateSummary",
202
+ Resource: "*"
203
+ },
184
204
  {
185
205
  Sid: "ReadRoleParam",
186
206
  Effect: "Allow",
@@ -201,9 +221,9 @@ async function ensureGithubRole(repo, bucket, appPrefix, appName, productId){
201
221
  PolicyDocument: JSON.stringify(policy)
202
222
  }));
203
223
 
204
- console.log(`Created role ${roleName} (${created.Role.Arn})`);
205
- console.log(`Use in GitHub Actions with: role-to-assume: ${created.Role.Arn}`);
206
- return created.Role.Arn;
224
+ console.log(`Configured role ${roleName} (${roleArn})`);
225
+ console.log(`Use in GitHub Actions with: role-to-assume: ${roleArn}`);
226
+ return roleArn;
207
227
  }
208
228
 
209
229
  async function ensureOidcProvider(iam, accountId){
@@ -1,9 +1,12 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
4
+ const { IAMClient, GetRoleCommand, UpdateAssumeRolePolicyCommand } = require("@aws-sdk/client-iam");
5
+ const { STSClient, GetCallerIdentityCommand } = require("@aws-sdk/client-sts");
4
6
  const OrgConfig = require('./helpers/org_config');
5
7
  const Apps = require('./helpers/apps');
6
8
  const Mime = require('./helpers/mime');
9
+ const { githubTrustPolicy } = require('./new_app');
7
10
 
8
11
  // Marketplace listing text files stored as top-level manifest attributes.
9
12
  // Names match the keys the AWS Marketplace Catalog API expects
@@ -27,25 +30,32 @@ exports.main = async function(args){
27
30
  if (!app){ throw new Error('No app named ' + args.app + '. Run new-app') }
28
31
  const { appPrefix, manifest } = app;
29
32
 
30
- if (!args.assets && !args.pid && !args.ec2){
31
- console.log('Nothing to update. Pass -assets, -pid, or -ec2');
33
+ if (!args.assets && !args.pid && !args.ec2 && !args.repo){
34
+ console.log('Nothing to update. Pass -assets, -pid, -ec2, or -repo');
32
35
  return true;
33
36
  }
34
37
 
35
38
  // Apply all manifest updates in one download/update/upload pass
36
- if (args.assets){
37
- await applyAssets(s3, bucket, appPrefix, cfg.region, args.assets, manifest);
38
- }
39
- if (args.pid){
40
- manifest.productId = args.pid;
41
- console.log('Set productId');
39
+ if (args.assets || args.pid || args.ec2){
40
+ if (args.assets){
41
+ await applyAssets(s3, bucket, appPrefix, cfg.region, args.assets, manifest);
42
+ }
43
+ if (args.pid){
44
+ manifest.productId = args.pid;
45
+ console.log('Set productId');
46
+ }
47
+ if (args.ec2){
48
+ manifest.RecommendedInstanceType = args.ec2;
49
+ console.log('Set RecommendedInstanceType');
50
+ }
51
+ await putManifest(s3, bucket, appPrefix, manifest);
52
+ console.log('Updated manifest');
42
53
  }
43
- if (args.ec2){
44
- manifest.RecommendedInstanceType = args.ec2;
45
- console.log('Set RecommendedInstanceType');
54
+
55
+ // Replace the CI role trust policy with one for the updated repo
56
+ if (args.repo){
57
+ await updateRepoTrust(appName, args.repo);
46
58
  }
47
- await putManifest(s3, bucket, appPrefix, manifest);
48
- console.log('Updated manifest');
49
59
  return true;
50
60
  }
51
61
 
@@ -115,6 +125,28 @@ async function applyAssets(s3, bucket, appPrefix, region, assetsPath, manifest){
115
125
  }
116
126
  }
117
127
 
128
+ async function updateRepoTrust(appName, repo){
129
+ if (!/^[\w.-]+\/[\w.-]+$/.test(repo)){ throw new Error('Invalid repo. Use org/repo format') }
130
+ const roleName = `mason-gha-${appName}`;
131
+ const iam = new IAMClient({ region: 'us-east-1' });
132
+ const sts = new STSClient({ region: 'us-east-1' });
133
+
134
+ try {
135
+ await iam.send(new GetRoleCommand({ RoleName: roleName }));
136
+ } catch (e){
137
+ if (/NoSuchEntity/.test(e)){ throw new Error(`No CI role ${roleName} found. Run new-app with -repo first`) }
138
+ throw e;
139
+ }
140
+
141
+ const accountId = (await sts.send(new GetCallerIdentityCommand({}))).Account;
142
+ const oidcArn = `arn:aws:iam::${accountId}:oidc-provider/token.actions.githubusercontent.com`;
143
+ await iam.send(new UpdateAssumeRolePolicyCommand({
144
+ RoleName: roleName,
145
+ PolicyDocument: JSON.stringify(githubTrustPolicy(oidcArn, repo))
146
+ }));
147
+ console.log(`Updated ${roleName} trust policy to repo ${repo}`);
148
+ }
149
+
118
150
  async function putManifest(s3, bucket, appPrefix, manifest){
119
151
  await s3.send(new PutObjectCommand({
120
152
  Bucket: bucket,
package/main.js CHANGED
@@ -39,7 +39,8 @@ const Commands = {
39
39
  {n: 'app', desc: 'Name of existing app', pattern: `^[A-Za-z]{2,20}$`, r: true},
40
40
  {n: 'assets', desc: 'Path to folder with application assets', r: false},
41
41
  {n: 'pid', desc: 'AWS Product ID', r: false},
42
- {n: 'ec2', desc: 'Instance type (default r8g.medium)', r: false}
42
+ {n: 'ec2', desc: 'Instance type (default r8g.medium)', r: false},
43
+ {n: 'repo', desc: 'GitHub repo (org/repo). Replaces the CI role trust policy with this repo', r: false}
43
44
  ]
44
45
  },
45
46
  'update-assets': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudmason2",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "",
5
5
  "main": "main.js",
6
6
  "files": [