@adobe/helix-deploy 6.0.1 → 6.1.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [6.1.0](https://github.com/adobe/helix-deploy/compare/v6.0.1...v6.1.0) (2022-01-25)
2
+
3
+
4
+ ### Features
5
+
6
+ * add AWS lambda authorizers support ([#362](https://github.com/adobe/helix-deploy/issues/362)) ([72e4def](https://github.com/adobe/helix-deploy/commit/72e4def53c5cb175447d97b70ec1de17161c1f78)), closes [#261](https://github.com/adobe/helix-deploy/issues/261) [#260](https://github.com/adobe/helix-deploy/issues/260)
7
+
1
8
  ## [6.0.1](https://github.com/adobe/helix-deploy/compare/v6.0.0...v6.0.1) (2022-01-24)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-deploy",
3
- "version": "6.0.1",
3
+ "version": "6.1.0",
4
4
  "description": "Library and Commandline Tools to build and deploy OpenWhisk Actions",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/adobe/helix-deploy#readme",
@@ -90,7 +90,7 @@
90
90
  "mocha-junit-reporter": "2.0.2",
91
91
  "mocha-multi-reporters": "1.5.1",
92
92
  "nock": "13.2.2",
93
- "semantic-release": "18.0.1",
93
+ "semantic-release": "19.0.2",
94
94
  "sinon": "12.0.1",
95
95
  "yauzl": "2.10.0"
96
96
  },
package/src/cli.js CHANGED
@@ -41,7 +41,9 @@ export default class CLI {
41
41
  .env('HLX');
42
42
  BaseConfig.yarg(this._yargs);
43
43
  PLUGINS.forEach((PluginClass) => PluginClass.Config.yarg(this._yargs));
44
- this._yargs.help();
44
+ this._yargs
45
+ .wrap(Math.min(120, this._yargs.terminalWidth()))
46
+ .help();
45
47
  }
46
48
 
47
49
  prepare(args) {
@@ -24,6 +24,8 @@ export default class AWSConfig {
24
24
  createRoutes: false,
25
25
  lambdaFormat: DEFAULT_LAMBDA_FORMAT,
26
26
  parameterMgr: ['system', 'secret'],
27
+ createAuthorizer: '',
28
+ attachAuthorizer: '',
27
29
  });
28
30
  }
29
31
 
@@ -33,6 +35,8 @@ export default class AWSConfig {
33
35
  .withAWSRole(argv.awsRole)
34
36
  .withAWSApi(argv.awsApi)
35
37
  .withAWSLambdaFormat(argv.awsLambdaFormat)
38
+ .withAWSCreateAuthorizer(argv.awsCreateAuthorizer)
39
+ .withAWSAttachAuthorizer(argv.awsAttachAuthorizer)
36
40
  .withAWSCleanUpBuckets(argv.awsCleanupBuckets)
37
41
  .withAWSCleanUpIntegrations(argv.awsCleanupIntegrations)
38
42
  .withAWSCreateRoutes(argv.awsCreateRoutes)
@@ -79,9 +83,21 @@ export default class AWSConfig {
79
83
  return this;
80
84
  }
81
85
 
86
+ withAWSCreateAuthorizer(value) {
87
+ this.createAuthorizer = value;
88
+ return this;
89
+ }
90
+
91
+ withAWSAttachAuthorizer(value) {
92
+ this.attachAuthorizer = value;
93
+ return this;
94
+ }
95
+
82
96
  static yarg(yargs) {
83
97
  return yargs
84
- .group(['aws-region', 'aws-api', 'aws-role', 'aws-cleanup-buckets', 'aws-cleanup-integrations', 'aws-create-routes'], 'AWS Deployment Options')
98
+ .group(['aws-region', 'aws-api', 'aws-role', 'aws-cleanup-buckets', 'aws-cleanup-integrations',
99
+ 'aws-create-routes', 'aws-create-authorizer', 'aws-attach-authorizer', 'aws-lambda-format',
100
+ 'aws-parameter-manager'], 'AWS Deployment Options')
85
101
  .option('aws-region', {
86
102
  description: 'the AWS region to deploy lambda functions to',
87
103
  type: 'string',
@@ -113,6 +129,17 @@ export default class AWSConfig {
113
129
  type: 'string',
114
130
  default: DEFAULT_LAMBDA_FORMAT,
115
131
  })
132
+ .option('aws-create-authorizer', {
133
+ description: 'Creates API Gateway authorizer using lambda authorization with this function and the specified name. '
134
+ + 'The string can contain placeholders (note that all dots (\'.\') are replaced with underscores. '
135
+ // eslint-disable-next-line no-template-curly-in-string
136
+ + 'Example: "helix-authorizer_${version}".',
137
+ type: 'string',
138
+ })
139
+ .option('aws-attach-authorizer', {
140
+ description: 'Attach specified authorizer to routes during linking.',
141
+ type: 'string',
142
+ })
116
143
  .option('aws-cleanup-buckets', {
117
144
  description: 'Cleans up stray temporary S3 buckets',
118
145
  type: 'boolean',
@@ -29,14 +29,14 @@ import {
29
29
 
30
30
  import {
31
31
  ApiGatewayV2Client,
32
- CreateApiCommand,
32
+ CreateApiCommand, CreateAuthorizerCommand,
33
33
  CreateIntegrationCommand, CreateRouteCommand,
34
34
  CreateStageCommand,
35
35
  DeleteIntegrationCommand,
36
36
  GetApiCommand,
37
- GetApisCommand,
37
+ GetApisCommand, GetAuthorizersCommand,
38
38
  GetIntegrationsCommand, GetRoutesCommand,
39
- GetStagesCommand, UpdateRouteCommand,
39
+ GetStagesCommand, UpdateAuthorizerCommand, UpdateRouteCommand,
40
40
  } from '@aws-sdk/client-apigatewayv2';
41
41
 
42
42
  import { PutParameterCommand, SSMClient } from '@aws-sdk/client-ssm';
@@ -366,6 +366,20 @@ export default class AWSDeployer extends BaseDeployer {
366
366
  return routes;
367
367
  }
368
368
 
369
+ async fetchAuthorizers(ApiId) {
370
+ let nextToken;
371
+ const authorizers = [];
372
+ do {
373
+ const res = await this._api.send(new GetAuthorizersCommand({
374
+ ApiId,
375
+ NextToken: nextToken,
376
+ }));
377
+ authorizers.push(...res.Items);
378
+ nextToken = res.NextToken;
379
+ } while (nextToken);
380
+ return authorizers;
381
+ }
382
+
369
383
  async createAPI() {
370
384
  const { cfg } = this;
371
385
  const { ApiId, ApiEndpoint } = await this.initApiId();
@@ -404,8 +418,12 @@ export default class AWSDeployer extends BaseDeployer {
404
418
  const { IntegrationId } = integration;
405
419
  this.log.info('--: fetching existing routes...');
406
420
  const routes = await this.fetchRoutes(ApiId);
407
- await this.createOrUpdateRoute(routes, ApiId, IntegrationId, `ANY ${this.functionPath}/{path+}`);
408
- await this.createOrUpdateRoute(routes, ApiId, IntegrationId, `ANY ${this.functionPath}`);
421
+ const routeParams = {
422
+ ApiId,
423
+ Target: `integrations/${IntegrationId}`,
424
+ };
425
+ await this.createOrUpdateRoute(routes, routeParams, `ANY ${this.functionPath}/{path+}`);
426
+ await this.createOrUpdateRoute(routes, routeParams, `ANY ${this.functionPath}`);
409
427
  }
410
428
 
411
429
  // setup permissions for entire package.
@@ -581,25 +599,24 @@ export default class AWSDeployer extends BaseDeployer {
581
599
  this.log.info(chalk`{green ok}: deleted ${unused.length} unused integrations.`);
582
600
  }
583
601
 
584
- async createOrUpdateRoute(routes, ApiId, IntegrationId, RouteKey) {
602
+ async createOrUpdateRoute(routes, routeParams, RouteKey) {
585
603
  const existing = routes.find((r) => r.RouteKey === RouteKey);
604
+ const auth = routeParams.AuthorizerId ? chalk` {yellow (${routeParams.AuthorizerId})}` : '';
586
605
  if (existing) {
587
606
  this.log.info(chalk`--: updating route for: {blue ${existing.RouteKey}}...`);
588
607
  const res = await this._api.send(new UpdateRouteCommand({
589
- ApiId,
590
- RouteId: existing.RouteId,
608
+ ...routeParams,
591
609
  RouteKey,
592
- Target: `integrations/${IntegrationId}`,
610
+ RouteId: existing.RouteId,
593
611
  }));
594
- this.log.info(chalk`{green ok}: updated route for: {blue ${res.RouteKey}}`);
612
+ this.log.info(chalk`{green ok}: updated route for: {blue ${res.RouteKey}}${auth}`);
595
613
  } else {
596
614
  this.log.info(chalk`--: creating route for: {blue ${RouteKey}}...`);
597
615
  const res = await this._api.send(new CreateRouteCommand({
598
- ApiId,
616
+ ...routeParams,
599
617
  RouteKey,
600
- Target: `integrations/${IntegrationId}`,
601
618
  }));
602
- this.log.info(chalk`{green ok}: created route for: {blue ${res.RouteKey}}`);
619
+ this.log.info(chalk`{green ok}: created route for: {blue ${res.RouteKey}}${auth}`);
603
620
  }
604
621
  }
605
622
 
@@ -609,20 +626,22 @@ export default class AWSDeployer extends BaseDeployer {
609
626
  FunctionName: functionName,
610
627
  Name: name,
611
628
  }));
612
- this.log.info(chalk`--: updating alias for: {blue ${name}}...`);
629
+ this.log.info(chalk`--: updating alias {blue ${name}}...`);
613
630
  await this._lambda.send(new UpdateAliasCommand({
614
631
  FunctionName: functionName,
615
632
  Name: name,
616
633
  FunctionVersion: functionVersion,
617
634
  }));
635
+ this.log.info(chalk`{green ok:} updated alias {blue ${name}} to version {yellow ${functionVersion}}.`);
618
636
  } catch (e) {
619
637
  if (e.name === 'ResourceNotFoundException') {
620
- this.log.info(chalk`--: creating alias for: {blue ${name}}...`);
638
+ this.log.info(chalk`--: creating alias {blue ${name}}...`);
621
639
  await this._lambda.send(new CreateAliasCommand({
622
640
  FunctionName: functionName,
623
641
  Name: name,
624
642
  FunctionVersion: functionVersion,
625
643
  }));
644
+ this.log.info(chalk`{green ok:} created alias {blue ${name}} for version {yellow ${functionVersion}}.`);
626
645
  } else {
627
646
  this.log.error(`Unable to verify existence of Lambda alias ${name}`);
628
647
  throw e;
@@ -673,16 +692,33 @@ export default class AWSDeployer extends BaseDeployer {
673
692
  const { IntegrationId } = integration;
674
693
 
675
694
  // get all the routes
676
- this.log.info(chalk`--: patching routes ...`);
695
+ this.log.info(chalk`--: fetching routes ...`);
677
696
  const routes = await this.fetchRoutes(ApiId);
697
+ const routeParams = {
698
+ ApiId,
699
+ Target: `integrations/${IntegrationId}`,
700
+ AuthorizerId: undefined,
701
+ AuthorizationType: 'NONE',
702
+ };
703
+ if (this._cfg.attachAuthorizer) {
704
+ this.log.info(chalk`--: fetching authorizers...`);
705
+ const authorizers = await this.fetchAuthorizers(ApiId);
706
+ const authorizer = authorizers.find((info) => info.Name === this._cfg.attachAuthorizer);
707
+ if (!authorizer) {
708
+ throw Error(`Specified authorizer ${this._cfg.attachAuthorizer} does not exist in api ${ApiId}.`);
709
+ }
710
+ routeParams.AuthorizerId = authorizer.AuthorizerId;
711
+ routeParams.AuthorizationType = 'CUSTOM';
712
+ this.log.info(chalk`{green ok:} configuring routes with authorizer {blue ${this._cfg.attachAuthorizer}} {yellow ${authorizer.AuthorizerId}}`);
713
+ }
678
714
 
679
715
  // create routes for each symlink
680
716
  const sfx = this.getLinkVersions();
681
717
 
682
718
  for (const suffix of sfx) {
683
719
  // check if route already exists
684
- await this.createOrUpdateRoute(routes, ApiId, IntegrationId, `ANY /${cfg.packageName}/${cfg.baseName}/${suffix}`);
685
- await this.createOrUpdateRoute(routes, ApiId, IntegrationId, `ANY /${cfg.packageName}/${cfg.baseName}/${suffix}/{path+}`);
720
+ await this.createOrUpdateRoute(routes, routeParams, `ANY /${cfg.packageName}/${cfg.baseName}/${suffix}`);
721
+ await this.createOrUpdateRoute(routes, routeParams, `ANY /${cfg.packageName}/${cfg.baseName}/${suffix}/{path+}`);
686
722
 
687
723
  // create or update alias
688
724
  await this.createOrUpdateAlias(suffix.replace('.', '_'), functionName, incrementalVersion);
@@ -691,6 +727,68 @@ export default class AWSDeployer extends BaseDeployer {
691
727
  if (cleanup) {
692
728
  await this.cleanUpIntegrations(functionName);
693
729
  }
730
+
731
+ await this.updateAuthorizers(ApiId, functionName, aliasArn);
732
+ }
733
+
734
+ async updateAuthorizers(ApiId, functionName, aliasArn) {
735
+ const cfg = this._cfg;
736
+ if (!cfg.createAuthorizer) {
737
+ return;
738
+ }
739
+
740
+ const AUTH_URI_PREFIX = `arn:aws:apigateway:${cfg.region}:lambda:path/2015-03-31/functions/`;
741
+ const accountId = aliasArn.split(':')[4];
742
+ this.log.info(chalk`--: patching authorizers...`);
743
+ const authorizers = await this.fetchAuthorizers(ApiId);
744
+ const versions = this.getLinkVersions();
745
+ for (const version of versions) {
746
+ const props = {
747
+ ...this.cfg,
748
+ ...this.cfg.properties,
749
+ // overwrite version with link name
750
+ version,
751
+ };
752
+ const authorizerName = ActionBuilder.substitute(cfg.createAuthorizer, props).replace(/\./g, '_');
753
+ const existing = authorizers.find((info) => info.Name === authorizerName) || {};
754
+ let { AuthorizerId } = existing;
755
+ if (AuthorizerId) {
756
+ const res = await this._api.send(new UpdateAuthorizerCommand({
757
+ ApiId,
758
+ AuthorizerId,
759
+ AuthorizerUri: `${AUTH_URI_PREFIX}${aliasArn}/invocations`,
760
+ }));
761
+ this.log.info(chalk`{green ok}: updated authorizer: {blue ${res.Name}}`);
762
+ } else {
763
+ const res = await this._api.send(new CreateAuthorizerCommand({
764
+ ApiId,
765
+ AuthorizerPayloadFormatVersion: '2.0',
766
+ AuthorizerType: 'REQUEST',
767
+ AuthorizerUri: `${AUTH_URI_PREFIX}${aliasArn}/invocations`,
768
+ AuthorizerResultTtlInSeconds: 0,
769
+ EnableSimpleResponses: true,
770
+ IdentitySource: ['$request.header.Authorization'],
771
+ Name: authorizerName,
772
+ }));
773
+ AuthorizerId = res.AuthorizerId;
774
+ this.log.info(chalk`{green ok}: created authorizer: {blue ${res.Name}}`);
775
+ }
776
+
777
+ // add permission to alias for the API Gateway is allowed to invoke the authorized function
778
+ try {
779
+ const sourceArn = `arn:aws:execute-api:${this._cfg.region}:${accountId}:${ApiId}/authorizers/${AuthorizerId}`;
780
+ await this._lambda.send(new AddPermissionCommand({
781
+ FunctionName: aliasArn,
782
+ Action: 'lambda:InvokeFunction',
783
+ SourceArn: sourceArn,
784
+ Principal: 'apigateway.amazonaws.com',
785
+ StatementId: crypto.createHash('sha256').update(aliasArn + sourceArn).digest('hex'),
786
+ }));
787
+ this.log.info(chalk`{green ok:} added invoke permissions for ${sourceArn}`);
788
+ } catch (e) {
789
+ // ignore, most likely the permission already exists
790
+ }
791
+ }
694
792
  }
695
793
 
696
794
  async checkFunctionReady(arn) {