Skip to content
Open
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
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ jobs:
USER_STAC_ITEM_GEN_ROLE_ARN: ${{ vars.USER_STAC_ITEM_GEN_ROLE_ARN }}
USER_STAC_INBOUND_TOPIC_ARNS: ${{ vars.USER_STAC_INBOUND_TOPIC_ARNS }}
USER_STAC_COLLECTION_ID_REGISTRY: ${{ vars.USER_STAC_COLLECTION_ID_REGISTRY }}
USER_STAC_COLLECTION_TRANSACTIONS_AUTH_MODE: ${{ vars.USER_STAC_COLLECTION_TRANSACTIONS_AUTH_MODE }}
USER_STAC_COLLECTION_TRANSACTIONS_AUTH_SECRET_ARN: ${{ vars.USER_STAC_COLLECTION_TRANSACTIONS_AUTH_SECRET_ARN }}
USER_STAC_COLLECTION_TRANSACTIONS_ENABLED: ${{ vars.USER_STAC_COLLECTION_TRANSACTIONS_ENABLED }}
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
118 changes: 57 additions & 61 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,75 +1,71 @@
name: tests
name: Unit and runtime tests

permissions:
id-token: write # required for requesting the JWT
contents: read # required for actions/checkout
contents: read

on:
# Uncomment below for running it manually on the github UI
workflow_dispatch:
pull_request:
push:
branches: [main]
workflow_dispatch:

# Uncomment below for running it on a push in a specific branch
# push:
# branches:
# - "change-stac-api-url-stage"
jobs:
node-tests:
name: node-tests
runs-on: ubuntu-latest

# Uncomment below for running it as a cron job
# schedule:
# - cron: '15 16 * * 5'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

jobs:
python-job:
name: "PyTest tests"
runs-on: ubuntu-latest
strategy:
matrix:
include:
- environment: test
- environment: dev
environment: ${{ matrix.environment }}
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "20"
cache: npm

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test

steps:
- name: Checkout repository
uses: actions/checkout@v3
python-runtime-tests:
name: pytest (${{ matrix.runtime.name }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
runtime:
- name: eoapi-stac
path: cdk/runtimes/eoapi/stac
- name: dps-stac-item-generator
path: cdk/constructs/DpsStacItemGenerator/runtime

- name: Setup Python
uses: actions/setup-python@v3
with:
python-version: '3.12'
defaults:
run:
working-directory: ${{ matrix.runtime.path }}

- name: Assume Github OIDC role
uses: aws-actions/configure-aws-credentials@v2
with:
aws-region: us-west-2
role-to-assume: ${{ vars.MAAP_EOAPI_TEST_ROLE }}
role-session-name: maap-eoapi-tests-${{ matrix.environment }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r tests/requirements.txt
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"

- name: Run pytest
env:
INGESTOR_DOMAIN_NAME: ${{ vars.INGESTOR_DOMAIN_NAME }}
STAC_API_CUSTOM_DOMAIN_NAME: ${{ vars.STAC_API_CUSTOM_DOMAIN_NAME }}
TITILER_PGSTAC_API_CUSTOM_DOMAIN_NAME: ${{ vars.TITILER_PGSTAC_API_CUSTOM_DOMAIN_NAME }}
SECRET_ID: ${{ vars.SECRET_ID }}
run: |
pytest tests
- name: Set up uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true

- name: slack
if: always()
id: slack
uses: slackapi/slack-github-action@v1.24.0
with:
# Slack channel id, channel name, or user id to post message.
# See also: https://api.slack.com/methods/chat.postMessage#channels
# You can pass in multiple channels to post to by providing a comma-delimited list of channel IDs.
channel-id: ${{ vars.SLACK_CHANNEL_ID }}
# For posting a simple plain text message
slack-message: "GitHub build result: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}"
env:
SLACK_BOT_TOKEN: ${{ vars.SLACK_BOT_TOKEN }}
- name: Sync dependencies
run: uv sync --locked --dev


- name: Run pytest
run: uv run pytest
30 changes: 0 additions & 30 deletions .github/workflows/unit-tests.yml

This file was deleted.

6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ __pycache__
.venv
.env
.envrc
.env-test
.test-env
.DS_Store

Expand All @@ -17,3 +18,8 @@ cdk.out

# potentially installed packages
stac-browser/

# local docker compose state
.pgdata/

dev-docs/plans/
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,37 @@ This repository contains the AWS CDK code (written in typescript) used to deploy

Deployment happens through a github workflow manually triggered and defined in `.github/workflows/deploy.yaml`.

## User STAC collection transactions

The internal `userSTAC` deployment can now opt into collection-only STAC transactions. The public-facing stack stays on the same MAAP-owned runtime, but remains read-only unless transaction support is explicitly enabled.

Enable them with:

- `USER_STAC_COLLECTION_TRANSACTIONS_ENABLED=true`
- `USER_STAC_COLLECTION_TRANSACTIONS_AUTH_MODE=basic`

When enabled, this CDK stack creates and manages the Secrets Manager secret used for STAC basic auth by default, grants the STAC Lambda read access to it, and publishes the secret ARN to SSM at:

- `/maap-eoapi/<stage>/internal/stac-collection-transaction-auth-secret-arn`

You can still override the secret with `USER_STAC_COLLECTION_TRANSACTIONS_AUTH_SECRET_ARN` if you need to point at an existing secret instead.

The transaction auth secret must be a JSON object with string `username` and `password` fields.

### What to verify after deployment

For a transaction-enabled internal deployment, verify:

- `GET /conformance` includes `https://api.stacspec.org/v1.0.0/collections/extensions/transaction`
- OpenAPI advertises collection write routes only:
- `POST /collections`
- `PUT /collections/{collection_id}`
- `PATCH /collections/{collection_id}`
- `DELETE /collections/{collection_id}`
- unauthenticated collection writes return `401`
- authenticated collection writes succeed
- item write routes are absent from the contract and return `404` or `405` rather than exposing item transaction behavior


## Networking and accessibility of the database.

Expand Down
95 changes: 89 additions & 6 deletions cdk/PgStacInfra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
aws_lambda as lambda,
aws_rds as rds,
aws_s3 as s3,
aws_secretsmanager as secretsmanager,
aws_cloudfront as cloudfront,
aws_cloudfront_origins as origins,
aws_cloudwatch as cloudwatch,
Expand Down Expand Up @@ -89,14 +90,75 @@ export class PgStacInfra extends Stack {
: ec2.SubnetType.PRIVATE_WITH_EGRESS,
};

const transactionsConfig = stacApiConfig.transactions;
if (transactionsConfig && transactionsConfig.authMode !== "basic") {
throw new Error(
`Unsupported STAC collection transaction auth mode: ${transactionsConfig.authMode}`,
);
}

const transactionAuthSecret = transactionsConfig
? transactionsConfig.authSecretArn
? secretsmanager.Secret.fromSecretCompleteArn(
this,
"stac-collection-transaction-auth-secret",
transactionsConfig.authSecretArn,
)
: new secretsmanager.Secret(
this,
"stac-collection-transaction-auth-secret",
{
description: `Basic auth secret for MAAP ${type} STAC collection transactions (${stage})`,
secretName: `/maap-eoapi/${stage}/${type}/stac-collection-transaction-basic-auth`,
generateSecretString: {
secretStringTemplate: JSON.stringify({
username: `maap-${type}-stac-writer`,
}),
generateStringKey: "password",
excludePunctuation: true,
},
},
)
: undefined;

const stacEnabledExtensions = [
"query",
"sort",
"fields",
"filter",
"free_text",
"pagination",
"collection_search",
...(transactionsConfig ? ["collection_transaction"] : []),
];

const stacApiEnv: Record<string, string> = {
STAC_FASTAPI_TITLE: `MAAP ${type} STAC API (${stage})`,
STAC_FASTAPI_LANDING_ID: `maap-${type}-stac-api-${stage}`,
STAC_FASTAPI_DESCRIPTION: `The ${type} STAC API for the [MAAP project](https://maap-project.org)`,
STAC_FASTAPI_VERSION: version,
ENABLED_EXTENSIONS: stacEnabledExtensions.join(","),
...(transactionsConfig
? {
MAAP_TRANSACTION_AUTH_MODE: transactionsConfig.authMode,
MAAP_TRANSACTION_AUTH_SECRET_ARN:
transactionAuthSecret!.secretArn,
}
: {}),
};

const stacApiLambdaOptions: CustomLambdaFunctionProps = {
code: lambda.Code.fromDockerBuild(__dirname, {
file: "dockerfiles/Dockerfile.stac",
targetStage: "lambda",
buildArgs: { PYTHON_VERSION: "3.12" },
}),
handler: "eoapi.stac.handler.handler",
};

// STAC API
const stacApiLambda = new PgStacApiLambda(this, "pgstac-api", {
apiEnv: {
STAC_FASTAPI_TITLE: `MAAP ${type} STAC API (${stage})`,
STAC_FASTAPI_LANDING_ID: `maap-${type}-stac-api-${stage}`,
STAC_FASTAPI_DESCRIPTION: `The ${type} STAC API for the [MAAP project](https://maap-project.org)`,
STAC_FASTAPI_VERSION: version,
},
apiEnv: stacApiEnv,
vpc,
db: pgstacDb.connectionTarget,
dbSecret: pgstacDb.pgstacSecret,
Expand All @@ -113,6 +175,7 @@ export class PgStacInfra extends Stack {
})
: undefined,
enableSnapStart: true,
lambdaFunctionOptions: stacApiLambdaOptions,
});

stacApiLambda.lambdaFunction.connections.allowTo(
Expand All @@ -128,6 +191,16 @@ export class PgStacInfra extends Stack {
});
}

if (transactionAuthSecret) {
transactionAuthSecret.grantRead(stacApiLambda.lambdaFunction);

new ssm.StringParameter(this, "stac-collection-transaction-auth-secret-param", {
parameterName: `/maap-eoapi/${stage}/${type}/stac-collection-transaction-auth-secret-arn`,
stringValue: transactionAuthSecret.secretArn,
description: `Secrets Manager ARN for MAAP ${type} STAC collection transaction auth (${stage})`,
});
}

// titiler-pgstac
const titilerDataAccessRole = iam.Role.fromRoleArn(
this,
Expand All @@ -141,6 +214,7 @@ export class PgStacInfra extends Stack {
const titilerPgstacLambdaOptions: CustomLambdaFunctionProps = {
code: lambda.Code.fromDockerBuild(__dirname, {
file: "dockerfiles/Dockerfile.raster",
targetStage: "lambda",
buildArgs: { PYTHON_VERSION: "3.12" },
}),
handler: "handler.handler",
Expand Down Expand Up @@ -615,6 +689,15 @@ export interface Props extends StackProps {
* STAC API api gateway source ARN to be granted STAC API lambda invoke permission.
*/
integrationApiArn?: string;

/**
* Optional collection transaction support for the STAC API.
* When omitted, the API stays read-only.
*/
transactions?: {
authMode: "basic" | "jwt";
authSecretArn?: string;
};
};

/**
Expand Down
2 changes: 2 additions & 0 deletions cdk/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const {
userStacCollectionIdRegistry,
userStacInboundTopicArns,
userStacItemGenRoleArn,
userStacCollectionTransactions,
userStacStacApiCustomDomainName,
userStacTitilerPgStacApiCustomDomainName,
version,
Expand Down Expand Up @@ -108,6 +109,7 @@ const userInfrastructure = new PgStacInfra(app, buildStackName("userSTAC"), {
},
stacApiConfig: {
customDomainName: userStacStacApiCustomDomainName,
transactions: userStacCollectionTransactions,
},
titilerPgstacConfig: {
mosaicHost,
Expand Down
Loading
Loading