Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
7 changes: 2 additions & 5 deletions cdk/PgStacInfra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -662,9 +661,7 @@ export interface Props extends StackProps {
};
dpsStacItemGenConfig?: {
itemGenRoleArn: string;
allowedAccountBucketPairs?:
| Array<{ accountId: string; bucketArn: string }>
| undefined;
inboundTopicArns?: string[];
};
addStactoolsItemGenerator?: boolean | undefined;
}
4 changes: 2 additions & 2 deletions cdk/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const {
tags,
titilerDataAccessRoleArn,
titilerPgStacApiCustomDomainName,
userStacAllowedPublisherAccountBucketPairs,
userStacInboundTopicArns,
userStacItemGenRoleArn,
userStacStacApiCustomDomainName,
userStacTitilerPgStacApiCustomDomainName,
Expand Down Expand Up @@ -123,7 +123,7 @@ const userInfrastructure = new PgStacInfra(app, buildStackName("userSTAC"), {
...(userStacItemGenRoleArn && {
dpsStacItemGenConfig: {
itemGenRoleArn: userStacItemGenRoleArn,
allowedAccountBucketPairs: userStacAllowedPublisherAccountBucketPairs,
inboundTopicArns: userStacInboundTopicArns,
},
}),
terminationProtection: false,
Expand Down
16 changes: 8 additions & 8 deletions cdk/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
}

Expand Down
72 changes: 15 additions & 57 deletions cdk/constructs/DpsStacItemGenerator/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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",
Expand All @@ -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,
},
},
}),
);
});
}
}
}
Loading