From 4a9d98950109846e7893254fe1b71eac96931d74 Mon Sep 17 00:00:00 2001 From: Toby Buckley Date: Thu, 27 Jun 2024 18:48:33 -0700 Subject: [PATCH] Refactored servics --- src/control-plane/auth/cognito-auth.ts | 1 + src/control-plane/control-plane-api.ts | 228 +++--------------- src/control-plane/control-plane.ts | 102 ++++---- ...fig-service.ts => tenant-config.lambda.ts} | 10 +- .../tenant-config/tenant-config.service.ts | 51 ++++ .../tenant-management.lambda.ts} | 18 +- .../tenant-management.service.ts | 82 +++++++ .../tenant-management.table.ts} | 4 +- .../user-management.service.ts | 69 ++++++ src/utils/utils.ts | 28 +++ 10 files changed, 330 insertions(+), 263 deletions(-) rename src/control-plane/tenant-config/{tenant-config-service.ts => tenant-config.lambda.ts} (90%) create mode 100644 src/control-plane/tenant-config/tenant-config.service.ts rename src/control-plane/{services.ts => tenant-management/tenant-management.lambda.ts} (85%) create mode 100644 src/control-plane/tenant-management/tenant-management.service.ts rename src/control-plane/{tables.ts => tenant-management/tenant-management.table.ts} (92%) create mode 100644 src/control-plane/user-management/user-management.service.ts diff --git a/src/control-plane/auth/cognito-auth.ts b/src/control-plane/auth/cognito-auth.ts index aa9bd755..dca99a42 100644 --- a/src/control-plane/auth/cognito-auth.ts +++ b/src/control-plane/auth/cognito-auth.ts @@ -394,6 +394,7 @@ export class CognitoAuth extends Construct implements IAuth { ]; this.tokenEndpoint = `https://${userPoolDomain.domainName}.auth.${region}.amazoncognito.com/oauth2/token`; + // TODO: The caller should be surfacing these, not the implementor new cdk.CfnOutput(this, 'ControlPlaneIdpUserPoolId', { value: this.userPool.userPoolId, key: 'ControlPlaneIdpUserPoolId', diff --git a/src/control-plane/control-plane-api.ts b/src/control-plane/control-plane-api.ts index 48fd89a2..7a541668 100644 --- a/src/control-plane/control-plane-api.ts +++ b/src/control-plane/control-plane-api.ts @@ -5,28 +5,23 @@ import * as cdk from 'aws-cdk-lib'; import * as apigateway from 'aws-cdk-lib/aws-apigateway'; import * as apigatewayV2 from 'aws-cdk-lib/aws-apigatewayv2'; import * as apigatewayV2Authorizers from 'aws-cdk-lib/aws-apigatewayv2-authorizers'; -import * as apigatewayV2Integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations'; -import * as events from 'aws-cdk-lib/aws-events'; -import * as targets from 'aws-cdk-lib/aws-events-targets'; import { Function } from 'aws-cdk-lib/aws-lambda'; import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; import { NagSuppressions } from 'cdk-nag'; import { Construct } from 'constructs'; import { IAuth } from './auth/auth-interface'; -import { Services } from './services'; -import { addTemplateTag, conditionallyAddScope } from '../utils'; +import { addTemplateTag } from '../utils'; export interface ControlPlaneAPIProps { - readonly services: Services; readonly auth: IAuth; - readonly tenantConfigServiceLambda: Function; readonly disableAPILogging?: boolean; } export class ControlPlaneAPI extends Construct { apiUrl: any; + jwtAuthorizer: apigatewayV2.IHttpRouteAuthorizer; public readonly api: apigatewayV2.HttpApi; - public readonly tenantUpdateServiceTarget: events.IRuleTarget; + // public readonly tenantUpdateServiceTarget: events.IRuleTarget; constructor(scope: Construct, id: string, props: ControlPlaneAPIProps) { super(scope, id); addTemplateTag(this, 'ControlPlaneAPI'); @@ -76,7 +71,7 @@ export class ControlPlaneAPI extends Construct { key: 'controlPlaneAPIEndpoint', }); - const jwtAuthorizer = new apigatewayV2Authorizers.HttpJwtAuthorizer( + this.jwtAuthorizer = new apigatewayV2Authorizers.HttpJwtAuthorizer( 'tenantsAuthorizer', props.auth.jwtIssuer, { @@ -84,195 +79,30 @@ export class ControlPlaneAPI extends Construct { } ); - const tenantsHttpLambdaIntegration = new apigatewayV2Integrations.HttpLambdaIntegration( - 'tenantsHttpLambdaIntegration', - props.services.tenantManagementServices - ); - const tenantsPath = '/tenants'; - this.api.addRoutes({ - path: tenantsPath, - methods: [apigatewayV2.HttpMethod.GET], - integration: tenantsHttpLambdaIntegration, - authorizer: jwtAuthorizer, - authorizationScopes: conditionallyAddScope(props.auth.fetchAllTenantsScope), - }); - this.api.addRoutes({ - path: tenantsPath, - methods: [apigatewayV2.HttpMethod.POST], - integration: tenantsHttpLambdaIntegration, - authorizer: jwtAuthorizer, - authorizationScopes: conditionallyAddScope(props.auth.createTenantScope), - }); - - const tenantIdPath = `${tenantsPath}/{tenantId}`; - this.api.addRoutes({ - path: tenantIdPath, - methods: [apigatewayV2.HttpMethod.DELETE], - integration: tenantsHttpLambdaIntegration, - authorizer: jwtAuthorizer, - authorizationScopes: conditionallyAddScope(props.auth.deleteTenantScope), - }); - this.api.addRoutes({ - path: tenantIdPath, - methods: [apigatewayV2.HttpMethod.GET], - integration: tenantsHttpLambdaIntegration, - authorizer: jwtAuthorizer, - authorizationScopes: conditionallyAddScope(props.auth.fetchTenantScope), - }); - - this.api.addRoutes({ - path: tenantIdPath, - methods: [apigatewayV2.HttpMethod.PUT], - integration: tenantsHttpLambdaIntegration, - authorizer: jwtAuthorizer, - authorizationScopes: conditionallyAddScope(props.auth.updateTenantScope), - }); - - const connection = new events.Connection(this, 'connection', { - authorization: events.Authorization.oauth({ - authorizationEndpoint: props.auth.tokenEndpoint, - clientId: props.auth.machineClientId, - clientSecret: props.auth.machineClientSecret, - httpMethod: events.HttpMethod.POST, - bodyParameters: { - grant_type: events.HttpParameter.fromString('client_credentials'), - ...(props.auth.updateTenantScope && { - scope: events.HttpParameter.fromString(props.auth.updateTenantScope), - }), - }, - }), - }); - - const putTenantAPIDestination = new events.ApiDestination(this, 'destination', { - connection: connection, - httpMethod: events.HttpMethod.PUT, - endpoint: `${this.api.url}${tenantsPath.substring(1)}/*`, // skip the first '/' in tenantIdPath - }); - - this.tenantUpdateServiceTarget = new targets.ApiDestination(putTenantAPIDestination, { - pathParameterValues: ['$.detail.tenantId'], - event: events.RuleTargetInput.fromEventPath('$.detail.tenantOutput'), - }); - - this.api.addRoutes({ - path: `${tenantIdPath}/deactivate`, - methods: [apigatewayV2.HttpMethod.PUT], - integration: tenantsHttpLambdaIntegration, - authorizer: jwtAuthorizer, - authorizationScopes: conditionallyAddScope(props.auth.deactivateTenantScope), - }); - this.api.addRoutes({ - path: `${tenantIdPath}/activate`, - methods: [apigatewayV2.HttpMethod.PUT], - integration: tenantsHttpLambdaIntegration, - authorizer: jwtAuthorizer, - authorizationScopes: conditionallyAddScope(props.auth.activateTenantScope), - }); - - const usersPath = '/users'; - this.api.addRoutes({ - path: usersPath, - methods: [apigatewayV2.HttpMethod.POST], - integration: new apigatewayV2Integrations.HttpLambdaIntegration( - 'tenantsHttpLambdaIntegration', - props.auth.createUserFunction - ), - authorizer: jwtAuthorizer, - authorizationScopes: conditionallyAddScope(props.auth.createUserScope), - }); - this.api.addRoutes({ - path: usersPath, - methods: [apigatewayV2.HttpMethod.GET], - integration: new apigatewayV2Integrations.HttpLambdaIntegration( - 'tenantsHttpLambdaIntegration', - props.auth.fetchAllUsersFunction - ), - authorizer: jwtAuthorizer, - authorizationScopes: conditionallyAddScope(props.auth.fetchAllUsersScope), - }); - - const userIdPath = `${usersPath}/{userId}`; - this.api.addRoutes({ - path: userIdPath, - methods: [apigatewayV2.HttpMethod.GET], - integration: new apigatewayV2Integrations.HttpLambdaIntegration( - 'tenantsHttpLambdaIntegration', - props.auth.fetchUserFunction - ), - authorizer: jwtAuthorizer, - authorizationScopes: conditionallyAddScope(props.auth.fetchUserScope), - }); - this.api.addRoutes({ - path: userIdPath, - methods: [apigatewayV2.HttpMethod.PUT], - integration: new apigatewayV2Integrations.HttpLambdaIntegration( - 'tenantsHttpLambdaIntegration', - props.auth.updateUserFunction - ), - authorizer: jwtAuthorizer, - authorizationScopes: conditionallyAddScope(props.auth.updateUserScope), - }); - - this.api.addRoutes({ - path: userIdPath, - methods: [apigatewayV2.HttpMethod.DELETE], - integration: new apigatewayV2Integrations.HttpLambdaIntegration( - 'tenantsHttpLambdaIntegration', - props.auth.deleteUserFunction - ), - authorizer: jwtAuthorizer, - authorizationScopes: conditionallyAddScope(props.auth.deleteUserScope), - }); - - this.api.addRoutes({ - path: `${tenantIdPath}/disable`, - methods: [apigatewayV2.HttpMethod.PUT], - integration: new apigatewayV2Integrations.HttpLambdaIntegration( - 'tenantsHttpLambdaIntegration', - props.auth.disableUserFunction - ), - authorizer: jwtAuthorizer, - authorizationScopes: conditionallyAddScope(props.auth.disableUserScope), - }); - this.api.addRoutes({ - path: `${tenantIdPath}/enable`, - methods: [apigatewayV2.HttpMethod.PUT], - integration: new apigatewayV2Integrations.HttpLambdaIntegration( - 'tenantsHttpLambdaIntegration', - props.auth.enableUserFunction - ), - authorizer: jwtAuthorizer, - authorizationScopes: conditionallyAddScope(props.auth.enableUserScope), - }); - - const tenantConfigPath = '/tenant-config'; - const tenantConfigServiceHttpLambdaIntegration = - new apigatewayV2Integrations.HttpLambdaIntegration( - 'tenantConfigServiceHttpLambdaIntegration', - props.tenantConfigServiceLambda - ); - const [tenantConfigRoute] = this.api.addRoutes({ - path: tenantConfigPath, - methods: [apigatewayV2.HttpMethod.GET], - integration: tenantConfigServiceHttpLambdaIntegration, - }); - - const tenantConfigNameResourcePath = `${tenantConfigPath}/{tenantName}`; - const [tenantConfigNameResourceRoute] = this.api.addRoutes({ - path: tenantConfigNameResourcePath, - methods: [apigatewayV2.HttpMethod.GET], - integration: tenantConfigServiceHttpLambdaIntegration, - }); - - NagSuppressions.addResourceSuppressionsByPath( - cdk.Stack.of(this), - [tenantConfigNameResourceRoute.node.path, tenantConfigRoute.node.path], - [ - { - id: 'AwsSolutions-APIG4', - reason: 'The /tenant-config endpoint is a publicly available endpoint.', - }, - ] - ); + // const connection = new events.Connection(this, 'connection', { + // authorization: events.Authorization.oauth({ + // authorizationEndpoint: props.auth.tokenEndpoint, + // clientId: props.auth.machineClientId, + // clientSecret: props.auth.machineClientSecret, + // httpMethod: events.HttpMethod.POST, + // bodyParameters: { + // grant_type: events.HttpParameter.fromString('client_credentials'), + // ...(props.auth.updateTenantScope && { + // scope: events.HttpParameter.fromString(props.auth.updateTenantScope), + // }), + // }, + // }), + // }); + + // const putTenantAPIDestination = new events.ApiDestination(this, 'destination', { + // connection: connection, + // httpMethod: events.HttpMethod.PUT, + // endpoint: `${this.api.url}${tenantsPath.substring(1)}/*`, // skip the first '/' in tenantIdPath + // }); + + // this.tenantUpdateServiceTarget = new targets.ApiDestination(putTenantAPIDestination, { + // pathParameterValues: ['$.detail.tenantId'], + // event: events.RuleTargetInput.fromEventPath('$.detail.tenantOutput'), + // }); } } diff --git a/src/control-plane/control-plane.ts b/src/control-plane/control-plane.ts index 12b03633..64c96997 100644 --- a/src/control-plane/control-plane.ts +++ b/src/control-plane/control-plane.ts @@ -8,11 +8,14 @@ import { IAuth } from './auth/auth-interface'; import { CognitoAuth } from './auth/cognito-auth'; import { IBilling, BillingProvider } from './billing'; import { ControlPlaneAPI } from './control-plane-api'; -import { Services } from './services'; -import { Tables } from './tables'; -import { TenantConfigService } from './tenant-config/tenant-config-service'; +// import { Services } from './services'; +// import { Tables } from './tables'; +// import { TenantConfigLambdas } from './tenant-config/tenant-config-service'; import { DestroyPolicySetter } from '../cdk-aspect/destroy-policy-setter'; -import { addTemplateTag, DetailType, EventManager, IEventManager } from '../utils'; +import { addTemplateTag, EventManager, IEventManager } from '../utils'; +import { TenantManagementService } from './tenant-management/tenant-management.service'; +import { TenantConfigService } from './tenant-config/tenant-config.service'; +import { UserManagementService } from './user-management/user-management.service'; export interface ControlPlaneProps { /** @@ -61,10 +64,10 @@ export class ControlPlane extends Construct { */ readonly controlPlaneAPIGatewayUrl: string; - /** - * The Tables instance containing the DynamoDB tables for tenant data and configurations. - */ - readonly tables: Tables; + // /** + // * The Tables instance containing the DynamoDB tables for tenant data and configurations. + // */ + // readonly tables: Tables; /** * The EventManager instance that allows connecting to events flowing between @@ -93,75 +96,78 @@ export class ControlPlane extends Construct { role: systemAdminRoleName, }); - // todo: decompose 'Tables' into purpose-specific constructs (ex. TenantManagement) - this.tables = new Tables(this, 'tables-stack'); - - this.eventManager = props.eventManager ?? new EventManager(this, 'EventManager'); - - // todo: decompose 'Services' into purpose-specific constructs (ex. TenantManagement) - const services = new Services(this, 'services-stack', { - tables: this.tables, - eventManager: this.eventManager, + const api = new ControlPlaneAPI(this, 'controlPlaneApi', { + auth, + disableAPILogging: props.disableAPILogging, }); - const tenantConfigService = new TenantConfigService(this, 'auth-info-service-stack', { - tenantDetails: this.tables.tenantDetails, - tenantDetailsTenantNameColumn: this.tables.tenantNameColumn, - tenantConfigIndexName: this.tables.tenantConfigIndexName, - tenantDetailsTenantConfigColumn: this.tables.tenantConfigColumn, + const eventManager = props.eventManager ?? new EventManager(this, 'EventManager'); + const tenantManagementServices = new TenantManagementService( + this, + 'tenantManagementServicves', + { + api: api.api, + auth, + authorizer: api.jwtAuthorizer, + eventManager, + } + ); + + new TenantConfigService(this, 'tenantConfigService', { + api: api.api, + tenantManagementTable: tenantManagementServices.table, }); - // todo: decompose 'ControlPlaneAPI' into purpose-specific constructs (ex. TenantManagement) - const controlPlaneAPI = new ControlPlaneAPI(this, 'controlplane-api-stack', { - auth: auth, - disableAPILogging: props.disableAPILogging, - services: services, - tenantConfigServiceLambda: tenantConfigService.tenantConfigServiceLambda, + new UserManagementService(this, 'userManagementService', { + api: api.api, + auth, + jwtAuthorizer: api.jwtAuthorizer, }); - this.controlPlaneAPIGatewayUrl = controlPlaneAPI.apiUrl; + this.controlPlaneAPIGatewayUrl = api.apiUrl; - this.eventManager.addTargetToEvent( - this, - DetailType.PROVISION_SUCCESS, - controlPlaneAPI.tenantUpdateServiceTarget - ); + // eventManager.addTargetToEvent( + // this, + // DetailType.PROVISION_SUCCESS, + // api.tenantUpdateServiceTarget + // ); - this.eventManager.addTargetToEvent( - this, - DetailType.DEPROVISION_SUCCESS, - controlPlaneAPI.tenantUpdateServiceTarget - ); + // eventManager.addTargetToEvent( + // this, + // DetailType.DEPROVISION_SUCCESS, + // controlPlaneAPI.tenantUpdateServiceTarget + // ); - new cdk.CfnOutput(this, 'controlPlaneAPIGatewayUrl', { - value: controlPlaneAPI.apiUrl, - key: 'controlPlaneAPIGatewayUrl', - }); + // new cdk.CfnOutput(this, 'controlPlaneAPIGatewayUrl', { + // value: controlPlaneAPI.apiUrl, + // key: 'controlPlaneAPIGatewayUrl', + // }); new cdk.CfnOutput(this, 'eventBridgeArn', { - value: this.eventManager.busArn, + value: eventManager.busArn, key: 'eventBridgeArn', }); if (props.billing) { const billingTemplate = new BillingProvider(this, 'Billing', { billing: props.billing, - eventManager: this.eventManager, - controlPlaneAPI: controlPlaneAPI.api, + eventManager: eventManager, + controlPlaneAPI: api.api, }); if (billingTemplate.controlPlaneAPIBillingWebhookResourcePath) { new cdk.CfnOutput(this, 'billingWebhookURL', { - value: `${controlPlaneAPI.apiUrl}${billingTemplate.controlPlaneAPIBillingWebhookResourcePath}`, + value: `${api.apiUrl}${billingTemplate.controlPlaneAPIBillingWebhookResourcePath}`, key: 'billingWebhookURL', }); } } + this.eventManager = eventManager; // defined suppression here to suppress EventsRole Default policy // which gets updated in EventManager construct, but is part of ControlPlane API NagSuppressions.addResourceSuppressions( - controlPlaneAPI, + api, [ { id: 'AwsSolutions-IAM5', diff --git a/src/control-plane/tenant-config/tenant-config-service.ts b/src/control-plane/tenant-config/tenant-config.lambda.ts similarity index 90% rename from src/control-plane/tenant-config/tenant-config-service.ts rename to src/control-plane/tenant-config/tenant-config.lambda.ts index a4b1e50e..d9ca070f 100644 --- a/src/control-plane/tenant-config/tenant-config-service.ts +++ b/src/control-plane/tenant-config/tenant-config.lambda.ts @@ -17,8 +17,8 @@ export interface TenantConfigServiceProps { readonly tenantDetailsTenantConfigColumn: string; } -export class TenantConfigService extends Construct { - public readonly tenantConfigServiceLambda: lambda.Function; +export class TenantConfigLambdas extends Construct { + public readonly tenantConfigFunction: lambda.Function; constructor(scope: Construct, id: string, props: TenantConfigServiceProps) { super(scope, id); addTemplateTag(this, 'TenantConfigService'); @@ -28,7 +28,7 @@ export class TenantConfigService extends Construct { cdk.Stack.of(this).region }:017000801446:layer:AWSLambdaPowertoolsPythonV2:59`; - this.tenantConfigServiceLambda = new lambda_python.PythonFunction( + this.tenantConfigFunction = new lambda_python.PythonFunction( this, 'TenantConfigServiceLambda', { @@ -56,10 +56,10 @@ export class TenantConfigService extends Construct { } ); - props.tenantDetails.grantReadData(this.tenantConfigServiceLambda); + props.tenantDetails.grantReadData(this.tenantConfigFunction); NagSuppressions.addResourceSuppressions( - this.tenantConfigServiceLambda.role!, + this.tenantConfigFunction.role!, [ { id: 'AwsSolutions-IAM4', diff --git a/src/control-plane/tenant-config/tenant-config.service.ts b/src/control-plane/tenant-config/tenant-config.service.ts new file mode 100644 index 00000000..86241c66 --- /dev/null +++ b/src/control-plane/tenant-config/tenant-config.service.ts @@ -0,0 +1,51 @@ +import { Construct } from 'constructs'; +import * as apigatewayV2 from 'aws-cdk-lib/aws-apigatewayv2'; +import * as cdk from 'aws-cdk-lib'; +import { Route, generateRoutes } from '../../utils'; +import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations'; +import { TenantConfigLambdas } from './tenant-config.lambda'; +import { TenantManagementTable } from '../tenant-management/tenant-management.table'; +import { NagSuppressions } from 'cdk-nag'; + +export interface TenantConfigServiceProps { + api: apigatewayV2.HttpApi; + tenantManagementTable: TenantManagementTable; +} +export class TenantConfigService extends Construct { + constructor(scope: Construct, id: string, props: TenantConfigServiceProps) { + super(scope, id); + + const tenantConfigPath = '/tenant-config'; + const tenantConfigNameResourcePath = `${tenantConfigPath}/{tenantName}`; + const tenantConfigLambda = new TenantConfigLambdas(this, 'tenantConfigLambda', { + tenantConfigIndexName: props.tenantManagementTable.tenantConfigIndexName, + tenantDetails: props.tenantManagementTable.tenantDetails, + tenantDetailsTenantConfigColumn: props.tenantManagementTable.tenantConfigColumn, + tenantDetailsTenantNameColumn: props.tenantManagementTable.tenantNameColumn, + }); + const tenantConfigServiceHttpLambdaIntegration = new HttpLambdaIntegration( + 'tenantConfigServiceHttpLambdaIntegration', + tenantConfigLambda.tenantConfigFunction + ); + const routes: Route[] = [ + { + path: tenantConfigPath, + integration: tenantConfigServiceHttpLambdaIntegration, + method: apigatewayV2.HttpMethod.GET, + }, + { + path: tenantConfigNameResourcePath, + integration: tenantConfigServiceHttpLambdaIntegration, + method: apigatewayV2.HttpMethod.GET, + }, + ]; + const tenantConfigRoutes = generateRoutes(props.api, routes); + const paths = tenantConfigRoutes.map((r) => r.node.path); + NagSuppressions.addResourceSuppressionsByPath(cdk.Stack.of(this), paths, [ + { + id: 'AwsSolutions-APIG4', + reason: 'The /tenant-config endpoint is a publicly available endpoint.', + }, + ]); + } +} diff --git a/src/control-plane/services.ts b/src/control-plane/tenant-management/tenant-management.lambda.ts similarity index 85% rename from src/control-plane/services.ts rename to src/control-plane/tenant-management/tenant-management.lambda.ts index 4eee0f4a..6aab1c33 100644 --- a/src/control-plane/services.ts +++ b/src/control-plane/tenant-management/tenant-management.lambda.ts @@ -8,16 +8,16 @@ import { Role, ServicePrincipal, ManagedPolicy } from 'aws-cdk-lib/aws-iam'; import { Runtime, LayerVersion, Function } from 'aws-cdk-lib/aws-lambda'; import { NagSuppressions } from 'cdk-nag'; import { Construct } from 'constructs'; -import { Tables } from './tables'; -import { DetailType, IEventManager } from '../utils'; +import { DetailType, IEventManager } from '../../utils'; +import { TenantManagementTable } from './tenant-management.table'; export interface ServicesProps { - readonly tables: Tables; + readonly table: TenantManagementTable; readonly eventManager: IEventManager; } -export class Services extends Construct { - tenantManagementServices: Function; +export class TenantManagementLambda extends Construct { + tenantManagementFunction: Function; constructor(scope: Construct, id: string, props: ServicesProps) { super(scope, id); @@ -26,7 +26,7 @@ export class Services extends Construct { assumedBy: new ServicePrincipal('lambda.amazonaws.com'), }); - props.tables.tenantDetails.grantReadWriteData(tenantManagementExecRole); + props.table.tenantDetails.grantReadWriteData(tenantManagementExecRole); props.eventManager.grantPutEventsTo(tenantManagementExecRole); tenantManagementExecRole.addManagedPolicy( @@ -66,7 +66,7 @@ export class Services extends Construct { Stack.of(this).region }:017000801446:layer:AWSLambdaPowertoolsPythonV2:59`; - const tenantManagementServices = new PythonFunction(this, 'TenantManagementServices', { + const tenantManagementFunc = new PythonFunction(this, 'TenantManagementServices', { entry: path.join(__dirname, '../../resources/functions/tenant-management'), runtime: Runtime.PYTHON_3_12, index: 'index.py', @@ -79,7 +79,7 @@ export class Services extends Construct { environment: { EVENTBUS_NAME: props.eventManager.busName, EVENT_SOURCE: props.eventManager.controlPlaneEventSource, - TENANT_DETAILS_TABLE: props.tables.tenantDetails.tableName, + TENANT_DETAILS_TABLE: props.table.tenantDetails.tableName, ONBOARDING_DETAIL_TYPE: DetailType.ONBOARDING_REQUEST, OFFBOARDING_DETAIL_TYPE: DetailType.OFFBOARDING_REQUEST, ACTIVATE_DETAIL_TYPE: DetailType.ACTIVATE_REQUEST, @@ -87,6 +87,6 @@ export class Services extends Construct { }, }); - this.tenantManagementServices = tenantManagementServices; + this.tenantManagementFunction = tenantManagementFunc; } } diff --git a/src/control-plane/tenant-management/tenant-management.service.ts b/src/control-plane/tenant-management/tenant-management.service.ts new file mode 100644 index 00000000..d4cbb34e --- /dev/null +++ b/src/control-plane/tenant-management/tenant-management.service.ts @@ -0,0 +1,82 @@ +import * as apigatewayV2 from 'aws-cdk-lib/aws-apigatewayv2'; +import { Construct } from 'constructs'; +import { IEventManager, Route, generateRoutes } from '../../utils'; +import { TenantManagementLambda } from './tenant-management.lambda'; +import { TenantManagementTable } from './tenant-management.table'; +import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations'; +import { IAuth } from '../auth/auth-interface'; + +export interface TenantManagementServiceProps { + api: apigatewayV2.HttpApi; + auth: IAuth; + authorizer: apigatewayV2.IHttpRouteAuthorizer; + eventManager: IEventManager; +} + +export class TenantManagementService extends Construct { + table: TenantManagementTable; + constructor(scope: Construct, id: string, props: TenantManagementServiceProps) { + super(scope, id); + + const table = new TenantManagementTable(this, 'tenantManagementTable'); + const lambda = new TenantManagementLambda(this, 'tenantManagementLambda', { + eventManager: props.eventManager, + table, + }); + + const tenantsHttpLambdaIntegration = new HttpLambdaIntegration( + 'tenantsHttpLambdaIntegration', + lambda.tenantManagementFunction + ); + const tenantsPath = '/tenants'; + const tenantIdPath = `${tenantsPath}/{tenantId}`; + + const routes: Route[] = [ + { + method: apigatewayV2.HttpMethod.GET, + scope: props.auth.fetchAllTenantsScope, + path: tenantsPath, + integration: tenantsHttpLambdaIntegration, + }, + { + method: apigatewayV2.HttpMethod.POST, + scope: props.auth.createTenantScope, + path: tenantsPath, + integration: tenantsHttpLambdaIntegration, + }, + { + method: apigatewayV2.HttpMethod.DELETE, + scope: props.auth.deleteTenantScope, + path: tenantIdPath, + integration: tenantsHttpLambdaIntegration, + }, + { + method: apigatewayV2.HttpMethod.GET, + scope: props.auth.fetchTenantScope, + path: tenantIdPath, + integration: tenantsHttpLambdaIntegration, + }, + { + method: apigatewayV2.HttpMethod.PUT, + scope: props.auth.updateTenantScope, + path: tenantIdPath, + integration: tenantsHttpLambdaIntegration, + }, + { + method: apigatewayV2.HttpMethod.PUT, + scope: props.auth.deactivateTenantScope, + path: `${tenantIdPath}/deactivate`, + integration: tenantsHttpLambdaIntegration, + }, + { + method: apigatewayV2.HttpMethod.PUT, + scope: props.auth.activateTenantScope, + path: `${tenantIdPath}/activate`, + integration: tenantsHttpLambdaIntegration, + }, + ]; + + generateRoutes(props.api, routes, props.authorizer); + this.table = table; + } +} diff --git a/src/control-plane/tables.ts b/src/control-plane/tenant-management/tenant-management.table.ts similarity index 92% rename from src/control-plane/tables.ts rename to src/control-plane/tenant-management/tenant-management.table.ts index bd10686b..902c8d8c 100644 --- a/src/control-plane/tables.ts +++ b/src/control-plane/tenant-management/tenant-management.table.ts @@ -3,9 +3,9 @@ import { Table, AttributeType, ProjectionType } from 'aws-cdk-lib/aws-dynamodb'; import { Construct } from 'constructs'; -import { addTemplateTag } from '../utils'; +import { addTemplateTag } from '../../utils'; -export class Tables extends Construct { +export class TenantManagementTable extends Construct { public readonly tenantDetails: Table; public readonly tenantConfigIndexName: string = 'tenantConfigIndex'; diff --git a/src/control-plane/user-management/user-management.service.ts b/src/control-plane/user-management/user-management.service.ts new file mode 100644 index 00000000..f7af1d6e --- /dev/null +++ b/src/control-plane/user-management/user-management.service.ts @@ -0,0 +1,69 @@ +import { Construct } from 'constructs'; +import { IAuth } from '..'; +import * as apigatewayV2 from 'aws-cdk-lib/aws-apigatewayv2'; +import { Route, generateRoutes } from '../../utils'; +import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations'; + +export interface UserManagementServiceProps { + api: apigatewayV2.HttpApi; + auth: IAuth; + jwtAuthorizer: apigatewayV2.IHttpRouteAuthorizer; +} + +export class UserManagementService extends Construct { + constructor(scope: Construct, id: string, props: UserManagementServiceProps) { + super(scope, id); + + const usersPath = '/users'; + const userIdPath = `${usersPath}/{userId}`; + const routes: Route[] = [ + { + method: apigatewayV2.HttpMethod.POST, + scope: props.auth.createUserScope, + path: usersPath, + integration: new HttpLambdaIntegration( + 'tenantsHttpLambdaIntegration', + props.auth.createUserFunction + ), + }, + { + method: apigatewayV2.HttpMethod.GET, + scope: props.auth.fetchAllUsersScope, + path: usersPath, + integration: new HttpLambdaIntegration( + 'tenantsHttpLambdaIntegration', + props.auth.fetchAllUsersFunction + ), + }, + { + method: apigatewayV2.HttpMethod.POST, + scope: props.auth.fetchUserScope, + path: userIdPath, + integration: new HttpLambdaIntegration( + 'tenantsHttpLambdaIntegration', + props.auth.fetchUserFunction + ), + }, + { + method: apigatewayV2.HttpMethod.POST, + scope: props.auth.updateUserScope, + path: userIdPath, + integration: new HttpLambdaIntegration( + 'tenantsHttpLambdaIntegration', + props.auth.updateUserFunction + ), + }, + { + method: apigatewayV2.HttpMethod.POST, + scope: props.auth.deleteUserScope, + path: userIdPath, + integration: new HttpLambdaIntegration( + 'tenantsHttpLambdaIntegration', + props.auth.deleteUserFunction + ), + }, + ]; + + generateRoutes(props.api, routes, props.jwtAuthorizer); + } +} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 2a80cf9c..3d9b727f 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -3,6 +3,7 @@ import { Stack } from 'aws-cdk-lib'; import { Construct } from 'constructs'; +import * as apigatewayV2 from 'aws-cdk-lib/aws-apigatewayv2'; export const addTemplateTag = (construct: Construct, tag: string) => { const stackDesc = Stack.of(construct).templateOptions.description; @@ -73,3 +74,30 @@ export const conditionallyAddScope = (unknownScope?: string) => { } return []; }; + +export interface Route { + method: apigatewayV2.HttpMethod; + scope?: string; + path: string; + integration: apigatewayV2.HttpRouteIntegration; +} +export const generateRoutes = ( + api: apigatewayV2.HttpApi, + routes: Route[], + authorizer?: apigatewayV2.IHttpRouteAuthorizer +) => { + let allRoutes: apigatewayV2.HttpRoute[] = []; + routes.forEach((route) => { + allRoutes = [ + ...api.addRoutes({ + path: route.path, + methods: [route.method], + integration: route.integration, + authorizer: authorizer, + authorizationScopes: conditionallyAddScope(route.scope), + }), + ...allRoutes, + ]; + }); + return allRoutes; +};