diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6d01058..222805e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -44,7 +44,7 @@ jobs: TITILER_DATA_ACCESS_ROLE_ARN: ${{ vars.TITILER_DATA_ACCESS_ROLE_ARN }} TITILER_PGSTAC_API_CUSTOM_DOMAIN_NAME: ${{ vars.TITILER_PGSTAC_API_CUSTOM_DOMAIN_NAME }} USER_STAC_ITEM_GEN_ROLE_ARN: ${{ vars.USER_STAC_ITEM_GEN_ROLE_ARN }} - USER_STAC_ALLOWED_PUBLISHER_ACCOUNT_BUCKET_PAIRS: ${{ vars.USER_STAC_ALLOWED_PUBLISHER_ACCOUNT_BUCKET_PAIRS }} + USER_STAC_INBOUND_TOPIC_ARNS: ${{ vars.USER_STAC_INBOUND_TOPIC_ARNS }} USER_STAC_STAC_API_CUSTOM_DOMAIN_NAME: ${{ vars.USER_STAC_STAC_API_CUSTOM_DOMAIN_NAME }} USER_STAC_TITILER_PGSTAC_API_CUSTOM_DOMAIN_NAME: ${{ vars.USER_STAC_TITILER_PGSTAC_API_CUSTOM_DOMAIN_NAME }} WEB_ACL_ARN: ${{ vars.WEB_ACL_ARN }} diff --git a/cdk/PgStacInfra.ts b/cdk/PgStacInfra.ts index f2241a7..7717597 100644 --- a/cdk/PgStacInfra.ts +++ b/cdk/PgStacInfra.ts @@ -502,8 +502,7 @@ export class PgStacInfra extends Stack { { itemLoadTopicArn: stacLoader.topic.topicArn, roleArn: dpsStacItemGenConfig.itemGenRoleArn, - allowedAccountBucketPairs: - dpsStacItemGenConfig.allowedAccountBucketPairs, + inboundTopicArns: dpsStacItemGenConfig.inboundTopicArns, vpc, subnetSelection: apiSubnetSelection, stage, @@ -662,9 +661,7 @@ export interface Props extends StackProps { }; dpsStacItemGenConfig?: { itemGenRoleArn: string; - allowedAccountBucketPairs?: - | Array<{ accountId: string; bucketArn: string }> - | undefined; + inboundTopicArns?: string[]; }; addStactoolsItemGenerator?: boolean | undefined; } diff --git a/cdk/app.ts b/cdk/app.ts index 4376a7e..d48f717 100644 --- a/cdk/app.ts +++ b/cdk/app.ts @@ -27,7 +27,7 @@ const { tags, titilerDataAccessRoleArn, titilerPgStacApiCustomDomainName, - userStacAllowedPublisherAccountBucketPairs, + userStacInboundTopicArns, userStacItemGenRoleArn, userStacStacApiCustomDomainName, userStacTitilerPgStacApiCustomDomainName, @@ -123,7 +123,7 @@ const userInfrastructure = new PgStacInfra(app, buildStackName("userSTAC"), { ...(userStacItemGenRoleArn && { dpsStacItemGenConfig: { itemGenRoleArn: userStacItemGenRoleArn, - allowedAccountBucketPairs: userStacAllowedPublisherAccountBucketPairs, + inboundTopicArns: userStacInboundTopicArns, }, }), terminationProtection: false, diff --git a/cdk/config.ts b/cdk/config.ts index e1f12c9..42410e5 100644 --- a/cdk/config.ts +++ b/cdk/config.ts @@ -21,7 +21,7 @@ export class Config { readonly pgstacVersion: string; readonly webAclArn: string; readonly userStacItemGenRoleArn: string; - readonly userStacAllowedPublisherAccountBucketPairs: Array<{accountId: string; bucketArn: string}> | undefined; + readonly userStacInboundTopicArns: string[] | undefined; readonly userStacStacApiCustomDomainName: string | undefined; readonly userStacTitilerPgStacApiCustomDomainName: string | undefined; @@ -119,19 +119,19 @@ export class Config { this.userStacStacApiCustomDomainName = process.env.USER_STAC_STAC_API_CUSTOM_DOMAIN_NAME; this.userStacTitilerPgStacApiCustomDomainName = process.env.USER_STAC_TITILER_PGSTAC_API_CUSTOM_DOMAIN_NAME; - if (process.env.USER_STAC_ALLOWED_PUBLISHER_ACCOUNT_BUCKET_PAIRS) { + if (process.env.USER_STAC_INBOUND_TOPIC_ARNS) { try { - this.userStacAllowedPublisherAccountBucketPairs = JSON.parse( - process.env.USER_STAC_ALLOWED_PUBLISHER_ACCOUNT_BUCKET_PAIRS, - ) as Array<{accountId: string; bucketArn: string}>; + this.userStacInboundTopicArns = JSON.parse( + process.env.USER_STAC_INBOUND_TOPIC_ARNS, + ) as string[]; } catch (error) { throw new Error( - `Invalid JSON format for USER_STAC_ALLOWED_PUBLISHER_ACCOUNT_BUCKET_PAIRS: ${error}. ` + - `Expected format: [{"accountId": "123456789012", "bucketArn": "arn:aws:s3:::bucket-name"}, ...]` + `Invalid JSON format for USER_STAC_INBOUND_TOPIC_ARNS: ${error}. ` + + `Expected format: ["arn:aws:sns:us-west-2:123456789012:topic-name", ...]` ); } } else { - this.userStacAllowedPublisherAccountBucketPairs = undefined; + this.userStacInboundTopicArns = undefined; } } diff --git a/cdk/constructs/DpsStacItemGenerator/index.ts b/cdk/constructs/DpsStacItemGenerator/index.ts index c5db5b0..6fd15c3 100644 --- a/cdk/constructs/DpsStacItemGenerator/index.ts +++ b/cdk/constructs/DpsStacItemGenerator/index.ts @@ -1,6 +1,5 @@ import { aws_ec2 as ec2, - aws_iam as iam, aws_lambda as lambda, aws_sqs as sqs, aws_sns as sns, @@ -91,18 +90,16 @@ export interface DpsStacItemGeneratorProps { readonly itemLoadTopicArn: string; /** - * Array of account ID and bucket ARN pairs that are allowed to publish. + * ARNs of externally-managed SNS topics that trigger item generation. * - * Format: [{accountId: "123456789012", bucketArn: "arn:aws:s3:::bucket-name"}, ...] + * These topics are owned and managed in the account where the source S3 + * buckets live. The SQS queue will subscribe to each topic. For + * cross-account topics, the topic policy in the remote account must permit + * `sns:Subscribe` from this account. * - * This provides fine-grained control ensuring only specific buckets from - * specific accounts can trigger item generation, preventing cross-account - * privilege escalation. + * @default [] */ - readonly allowedAccountBucketPairs?: Array<{ - accountId: string; - bucketArn: string; - }>; + readonly inboundTopicArns?: string[]; readonly roleArn: string; /** @@ -134,14 +131,6 @@ export class DpsStacItemGenerator extends Construct { */ public readonly deadLetterQueue: sqs.Queue; - /** - * The SNS topic that receives item generation requests. - * - * External systems publish ItemRequest messages to this topic to trigger - * STAC item generation. The topic fans out to the SQS queue for processing. - */ - public readonly topic: sns.Topic; - /** * The Lambda function that generates STAC items */ @@ -171,19 +160,16 @@ export class DpsStacItemGenerator extends Construct { }, }); - // Create SNS topic - this.topic = new sns.Topic(this, "Topic", { - displayName: `${id}-ItemGenTopic`, + // Subscribe the queue to each externally-managed inbound topic + (props.inboundTopicArns ?? []).forEach((topicArn, index) => { + const topic = sns.Topic.fromTopicArn( + this, + `InboundTopic${index}`, + topicArn, + ); + topic.addSubscription(new snsSubscriptions.SqsSubscription(this.queue)); }); - // Add cross-account access policies - this.configureCrossAccountAccess(props); - - // Subscribe the queue to the topic - this.topic.addSubscription( - new snsSubscriptions.SqsSubscription(this.queue), - ); - this.lambdaFunction = new lambda.Function(this, "Function", { runtime: lambdaRuntime, role: Role.fromRoleArn(this, "dps-item-gen-role", props.roleArn), @@ -219,12 +205,6 @@ export class DpsStacItemGenerator extends Construct { // The consuming construct should handle this permission // Create outputs - new CfnOutput(this, "TopicArn", { - value: this.topic.topicArn, - description: "ARN of the DpsStacItemGenerator SNS Topic", - exportName: `dps-stac-item-generator-topic-arn-${stage}`, - }); - new CfnOutput(this, "QueueUrl", { value: this.queue.queueUrl, description: "URL of the DpsStacItemGenerator SQS Queue", @@ -243,26 +223,4 @@ export class DpsStacItemGenerator extends Construct { exportName: `dps-stac-item-generator-function-name-${stage}`, }); } - - private configureCrossAccountAccess(props: DpsStacItemGeneratorProps) { - if (props.allowedAccountBucketPairs?.length) { - props.allowedAccountBucketPairs.forEach((pair, index) => { - this.topic.addToResourcePolicy( - new iam.PolicyStatement({ - sid: `AllowAccountBucketPair${index}Publish`, - effect: iam.Effect.ALLOW, - principals: [new iam.ServicePrincipal("s3.amazonaws.com")], - actions: ["SNS:Publish"], - resources: [this.topic.topicArn], - conditions: { - StringEquals: { - "aws:SourceArn": pair.bucketArn, - // "aws:SourceAccount": pair.accountId, - }, - }, - }), - ); - }); - } - } }