From c57422ee2f56e96bbf08f0a607ef941f305cb032 Mon Sep 17 00:00:00 2001 From: Victor San Kho Lin Date: Thu, 21 Mar 2024 11:13:59 +1100 Subject: [PATCH] Improved sequence_run_manager API Gateway setup Related #139 --- .../stateless/sequence_run_manager/api.py | 2 +- .../deploy/apigw/component.ts | 39 ++++++++++++++----- .../sequence_run_manager/deploy/component.ts | 19 ++++++--- .../sequence_run_manager/urls/base.py | 6 ++- .../sequence_run_manager/urls/local.py | 6 +-- skel/django-api/src/api.py | 2 +- 6 files changed, 54 insertions(+), 20 deletions(-) diff --git a/lib/workload/stateless/sequence_run_manager/api.py b/lib/workload/stateless/sequence_run_manager/api.py index efe7d2752..2037c2d49 100644 --- a/lib/workload/stateless/sequence_run_manager/api.py +++ b/lib/workload/stateless/sequence_run_manager/api.py @@ -9,4 +9,4 @@ def handler(event, context): - return serverless_wsgi.handle_request(application.app, event, context) + return serverless_wsgi.handle_request(application, event, context) diff --git a/lib/workload/stateless/sequence_run_manager/deploy/apigw/component.ts b/lib/workload/stateless/sequence_run_manager/deploy/apigw/component.ts index d5f843e59..a57307832 100644 --- a/lib/workload/stateless/sequence_run_manager/deploy/apigw/component.ts +++ b/lib/workload/stateless/sequence_run_manager/deploy/apigw/component.ts @@ -1,22 +1,17 @@ import { Construct } from 'constructs'; import { aws_ssm, Duration } from 'aws-cdk-lib'; -import * as cognito from 'aws-cdk-lib/aws-cognito'; -import { HttpUserPoolAuthorizer } from 'aws-cdk-lib/aws-apigatewayv2-authorizers'; +import { HttpJwtAuthorizer } from 'aws-cdk-lib/aws-apigatewayv2-authorizers'; import { CorsHttpMethod, HttpApi } from 'aws-cdk-lib/aws-apigatewayv2'; import { IStringParameter } from 'aws-cdk-lib/aws-ssm'; export class SRMApiGatewayConstruct extends Construct { - private readonly SSM_USER_POOL_ID: string = '/data_portal/client/cog_user_pool_id'; // FIXME one fine day in future private readonly _httpApi: HttpApi; - constructor(scope: Construct, id: string) { + constructor(scope: Construct, id: string, apiName: string, region: string) { super(scope, id); - const userPoolParam: IStringParameter = aws_ssm.StringParameter.fromStringParameterName(this, id + 'SSMStringParameter', this.SSM_USER_POOL_ID); - const userPool = cognito.UserPool.fromUserPoolId(scope, id + 'UserPool', userPoolParam.stringValue); - this._httpApi = new HttpApi(this, id + 'HttpApi', { - apiName: 'OrcaBus SequenceRunManager API', + apiName: 'OrcaBusAPI' + apiName, corsPreflight: { allowHeaders: ['Authorization'], allowMethods: [ @@ -28,13 +23,39 @@ export class SRMApiGatewayConstruct extends Construct { allowOrigins: ['*'], // FIXME allowed origins from config constant maxAge: Duration.days(10), }, - defaultAuthorizer: new HttpUserPoolAuthorizer(id + 'HttpUserPoolAuthorizer', userPool), + defaultAuthorizer: this.getAuthorizer(id, region), // defaultDomainMapping: ... TODO }); + // TODO Configure access logging. See https://github.com/aws/aws-cdk/issues/11100 + // TODO setup cloud map service discovery perhaps } + private getAuthorizer(id: string, region: string): HttpJwtAuthorizer { + /** + * FIXME One fine day in future when we have proper Cognito AAI setup. + * For the moment, we leverage Portal and established Cognito infrastructure. + * See https://github.com/umccr/orcabus/issues/102 + */ + const SSM_USER_POOL_ID: string = '/data_portal/client/cog_user_pool_id'; + const SSM_PORTAL_CLIENT_ID: string = '/data_portal/client/data2/cog_app_client_id_stage'; // i.e. JWT from UI client https://portal.[dev|stg|prod].umccr.org + const SSM_STATUS_PAGE_CLIENT_ID: string = '/data_portal/status_page/cog_app_client_id_stage'; // i.e. JWT from UI client https://status.[dev|stg|prod].umccr.org + + const userPoolIdParam: IStringParameter = aws_ssm.StringParameter.fromStringParameterName(this, id + 'CognitoUserPoolIdParameter', SSM_USER_POOL_ID); + const portalClientIdParam: IStringParameter = aws_ssm.StringParameter.fromStringParameterName(this, id + 'CognitoPortalClientIdParameter', SSM_PORTAL_CLIENT_ID); + const statusPageClientIdParam: IStringParameter = aws_ssm.StringParameter.fromStringParameterName(this, id + 'CognitoStatusPageClientIdParameter', SSM_STATUS_PAGE_CLIENT_ID); + + const issuer = 'https://cognito-idp.' + region + '.amazonaws.com/' + userPoolIdParam.stringValue; + + return new HttpJwtAuthorizer(id + 'PortalAuthorizer', issuer, { + jwtAudience: [ + portalClientIdParam.stringValue, + statusPageClientIdParam.stringValue, + ], + }); + } + get httpApi(): HttpApi { return this._httpApi; } diff --git a/lib/workload/stateless/sequence_run_manager/deploy/component.ts b/lib/workload/stateless/sequence_run_manager/deploy/component.ts index eb1fc59c0..d14ef92ed 100644 --- a/lib/workload/stateless/sequence_run_manager/deploy/component.ts +++ b/lib/workload/stateless/sequence_run_manager/deploy/component.ts @@ -1,12 +1,12 @@ import path from 'path'; import * as cdk from 'aws-cdk-lib'; -import { aws_lambda, aws_secretsmanager, Stack } from 'aws-cdk-lib'; +import { aws_lambda, aws_secretsmanager, Duration, Stack } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; import { IEventBus } from 'aws-cdk-lib/aws-events'; import { PythonFunction, PythonLayerVersion } from '@aws-cdk/aws-lambda-python-alpha'; import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations'; -import { HttpMethod, HttpRoute, HttpRouteKey, HttpStage } from 'aws-cdk-lib/aws-apigatewayv2'; +import { HttpMethod, HttpRoute, HttpRouteKey } from 'aws-cdk-lib/aws-apigatewayv2'; import { ManagedPolicy, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; import { SRMApiGatewayConstruct } from './apigw/component'; import { Architecture } from 'aws-cdk-lib/aws-lambda'; @@ -20,7 +20,7 @@ export interface SequenceRunManagerProps { export class SequenceRunManagerStack extends Stack { // Follow by naming convention. See https://github.com/umccr/orcabus/pull/149 private readonly secretId: string = 'orcabus/sequence_run_manager/rdsLoginCredential'; - private readonly apiNamespace: string = '/srm/v1'; + private readonly apiName: string = 'SequenceRunManager'; // apiNamespace `/srm/v1` is handled by Django Router private readonly id: string; private readonly props: SequenceRunManagerProps; private readonly baseLayer: PythonLayerVersion; @@ -38,12 +38,18 @@ export class SequenceRunManagerStack extends Stack { assumedBy: new ServicePrincipal('lambda.amazonaws.com'), description: 'Lambda execution role for ' + this.id, }); + // FIXME it is best practise to such that we do not use AWS managed policy + // we should improve this at some point down the track. + // See https://github.com/umccr/orcabus/issues/174 this.lambdaRole.addManagedPolicy( ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'), ); this.lambdaRole.addManagedPolicy( ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaVPCAccessExecutionRole'), ); + this.lambdaRole.addManagedPolicy( + ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMReadOnlyAccess'), + ); const dbSecret = aws_secretsmanager.Secret.fromSecretNameV2(this, this.id + 'dbSecret', this.secretId); dbSecret.grantRead(this.lambdaRole); @@ -84,6 +90,7 @@ export class SequenceRunManagerStack extends Stack { this.createPythonFunction('Migration', { index: 'migrate.py', handler: 'handler', + timeout: Duration.minutes(2), }); } @@ -91,9 +98,10 @@ export class SequenceRunManagerStack extends Stack { const apiFn: PythonFunction = this.createPythonFunction('Api', { index: 'api.py', handler: 'handler', + timeout: Duration.seconds(28), }); - const srmApi = new SRMApiGatewayConstruct(this, this.id + 'SRMApiGatewayConstruct'); + const srmApi = new SRMApiGatewayConstruct(this, this.id + 'SRMApiGatewayConstruct', this.apiName, this.region); const httpApi = srmApi.httpApi; const apiIntegration = new HttpLambdaIntegration(this.id + 'ApiIntegration', apiFn); @@ -101,7 +109,7 @@ export class SequenceRunManagerStack extends Stack { new HttpRoute(this, this.id + 'HttpRoute', { httpApi: httpApi, integration: apiIntegration, - routeKey: HttpRouteKey.with(this.apiNamespace + '/{proxy+}', HttpMethod.ANY), + routeKey: HttpRouteKey.with('/{proxy+}', HttpMethod.ANY), }); } @@ -109,6 +117,7 @@ export class SequenceRunManagerStack extends Stack { const procSqsFn = this.createPythonFunction('ProcHandler', { index: 'sequence_run_manager_proc/lambdas/bssh_event.py', handler: 'sqs_handler', + timeout: Duration.minutes(2), }); this.props.mainBus.grantPutEventsTo(procSqsFn); diff --git a/lib/workload/stateless/sequence_run_manager/sequence_run_manager/urls/base.py b/lib/workload/stateless/sequence_run_manager/sequence_run_manager/urls/base.py index 69c06911b..d8e264a10 100644 --- a/lib/workload/stateless/sequence_run_manager/sequence_run_manager/urls/base.py +++ b/lib/workload/stateless/sequence_run_manager/sequence_run_manager/urls/base.py @@ -3,12 +3,16 @@ from sequence_run_manager.routers import OptionalSlashDefaultRouter from sequence_run_manager.viewsets.sequence import SequenceViewSet +api_namespace = "srm" +api_version = "v1" +api_base = f"{api_namespace}/{api_version}/" + router = OptionalSlashDefaultRouter() router.register(r"sequence", SequenceViewSet, basename="sequence") urlpatterns = [ # path("iam/", include(router.urls)), - path("", include(router.urls)), + path(f"{api_base}", include(router.urls)), ] handler500 = "rest_framework.exceptions.server_error" diff --git a/lib/workload/stateless/sequence_run_manager/sequence_run_manager/urls/local.py b/lib/workload/stateless/sequence_run_manager/sequence_run_manager/urls/local.py index dc2c986d5..3de1e81f4 100644 --- a/lib/workload/stateless/sequence_run_manager/sequence_run_manager/urls/local.py +++ b/lib/workload/stateless/sequence_run_manager/sequence_run_manager/urls/local.py @@ -3,12 +3,12 @@ from drf_yasg.views import get_schema_view from rest_framework import permissions -from .base import urlpatterns as base_urlpatterns, router +from .base import urlpatterns as base_urlpatterns, router, api_base, api_version schema_view = get_schema_view( openapi.Info( title="UMCCR OrcaBus sequence_run_manager API", - default_version="v1", + default_version=f"{api_version}", description="UMCCR OrcaBus sequence_run_manager API", terms_of_service="https://umccr.org/", contact=openapi.Contact(email="services@umccr.org"), @@ -19,7 +19,7 @@ permissions.AllowAny, ], patterns=[ - path("", include(router.urls)), + path(f"{api_base}", include(router.urls)), ], ) diff --git a/skel/django-api/src/api.py b/skel/django-api/src/api.py index 7055681c8..359f7550e 100644 --- a/skel/django-api/src/api.py +++ b/skel/django-api/src/api.py @@ -9,4 +9,4 @@ def handler(event, context): - return serverless_wsgi.handle_request(application.app, event, context) + return serverless_wsgi.handle_request(application, event, context)