diff --git a/README.md b/README.md index a8a0270d..e39a430e 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,204 @@ -# STAC Auth Proxy +
+

stac auth proxy

+

Reverse proxy to apply auth*n scenarios to STAC APIs.

+
+ +--- > [!WARNING] > This project is currently in active development and may change drastically in the near future while we work towards solidifying a first release. -STAC Auth Proxy is a proxy API that mediates between the client and and some internally accessible STAC API in order to provide a flexible authentication mechanism. +STAC Auth Proxy is a proxy API that mediates between the client and an internally accessible STAC API in order to provide a flexible authentication, authorization, and content filtering mechanism. ## Features -- 🔐 Selectively apply OIDC auth to some or all endpoints & methods -- 📖 Augments [OpenAPI](https://swagger.io/specification/) with auth information, keeping auto-generated docs (e.g. [Swagger UI](https://swagger.io/tools/swagger-ui/)) accurate +- 🔐 Authentication: Selectively apply OIDC auth to some or all endpoints & methods +- 🎟️ Content Filtering: Apply CQL2 filters to client requests, filtering API content based on user context +- 📖 OpenAPI Augmentation: Update [OpenAPI](https://swagger.io/specification/) with security requirements, keeping auto-generated docs (e.g. [Swagger UI](https://swagger.io/tools/swagger-ui/)) accurate -### CQL2 Filters +## Usage -| Method | Endpoint | Action | Filter | Strategy | -| -------- | ---------------------------------------------- | ------ | ------ | ---------------------------------------------------------------------------------------------------------- | -| `POST` | `/search` | Read | Item | Append body with generated CQL2 query. | -| `GET` | `/search` | Read | Item | Append query params with generated CQL2 query. | -| `GET` | `/collections/{collection_id}/items` | Read | Item | Append query params with generated CQL2 query. | -| `POST` | `/collections/{collection_id}/items` | Create | Item | Validate body with generated CQL2 query. | -| `PUT` | `/collections/{collection_id}/items/{item_id}` | Update | Item | Fetch STAC Item and validate CQL2 query; merge STAC Item with body and validate with generated CQL2 query. | -| `DELETE` | `/collections/{collection_id}/items/{item_id}` | Delete | Item | Fetch STAC Item and validate with CQL2 query. | +> [!NOTE] +> Currently, the project is only installable by downlaoding the repository. It will eventually be available on Docker ([#5](https://github.com/developmentseed/issues/5)) and PyPi ([#30](https://github.com/developmentseed/issues/30)). -#### Recipes +### Installation -Only return collections that are mentioned in a `collections` array encoded within the auth token. +For local development, his project uses [`uv`](https://docs.astral.sh/uv/) to manage project dependencies and environment. +```sh +uv sync ``` -"A_CONTAINEDBY(id, ('{{ token.collections | join(\"', '\") }}' ))" + +Otherwise, the application can be installed as a standard Python module: + +```sh +python3 install src ``` -## Installation +### Running -Set up connection to upstream STAC API and the OpenID Connect provider by setting the following environment variables: +The simplest way to run the project is by calling the module directly: -```bash -export STAC_AUTH_PROXY_UPSTREAM_URL="https://some.url" -export STAC_AUTH_PROXY_OIDC_DISCOVERY_URL="https://your-openid-connect-provider.com/.well-known/openid-configuration" +```sh +python -m stac_auth_proxy ``` -Install software: +Alternatively, the application's factory can be passed to Uvicorn: -```bash -uv run python -m stac_auth_proxy +```sh +uvicorn --factory stac_auth_proxy:create_app ``` + +### Configuration + +The application is configurable via environment variables. + +- `UPSTREAM_URL` + - The STAC API to proxy requests to + - **Type:** HTTP(S) URL + - **Required:** Yes + - **Example:** `https://your-stac-api.com/stac` +- `OIDC_DISCOVERY_URL` + - OpenID Connect discovery document URL + - **Type:** HTTP(S) URL + - **Required:** Yes + - **Example:** `https://auth.example.com/.well-known/openid-configuration` +- `OIDC_DISCOVERY_INTERNAL_URL` + - The internal network OpenID Connect discovery document URL + - **Type:** HTTP(S) URL + - **Required:** No, defaults to value of `OIDC_DISCOVERY_URL` + - **Example:** `http://auth/.well-known/openid-configuration` +- `DEFAULT_PUBLIC` + - **Description:** Default access policy for endpoints + - **Type:** boolean + - **Default:** `false` + - **Example:** `false`, `1`, `True` +- `PRIVATE_ENDPOINTS` + - **Description:** Endpoints explicitely marked as requiring authentication, for use when `DEFAULT_PUBLIC == True` + - **Type:** JSON object mapping regex patterns to HTTP methods OR to tuples of HTTP methods and an array of strings representing required scopes. + - **Default:** + ```json + { + "^/collections$": ["POST"], + "^/collections/([^/]+)$": ["PUT", "PATCH", "DELETE"], + "^/collections/([^/]+)/items$": ["POST"], + "^/collections/([^/]+)/items/([^/]+)$": ["PUT", "PATCH", "DELETE"], + "^/collections/([^/]+)/bulk_items$": ["POST"] + } + ``` +- `PUBLIC_ENDPOINTS` + - **Description:** Endpoints explicitely marked as not requiring authentication, for use when `DEFAULT_PUBLIC == False` + - **Type:** JSON object mapping regex patterns to HTTP methods + - **Default:** + ```json + { + "^/api.html$": ["GET"], + "^/api$": ["GET"] + } + ``` +- `OPENAPI_SPEC_ENDPOINT` + - Path to serve OpenAPI specification + - **Type:** string or null + - **Default:** `null` (disabled) + - **Example:** `/api` +- `ITEMS_FILTER` + - Configuration for item-level filtering + - **Type:** JSON object with class configuration + - **Default:** `null` + - Components: + - `cls`: Python import path + - `args`: List of positional arguments + - `kwargs`: Dictionary of keyword arguments + - **Example:** + ```json + { + "cls": "my_package.filters.OrganizationFilter", + "args": ["org1"], + "kwargs": { + "field_name": "properties.organization" + } + } + ``` +- `ITEMS_FILTER_ENDPOINTS` + - Where to apply item filtering + - **Type:** JSON object mapping regex patterns to HTTP methods + - **Default:** + ```json + { + "^/search$": ["POST"], + "^/collections/([^/]+)/items$": ["GET", "POST"] + } + ``` + +## Architecture + +### Middleware Stack + +The middleware stack is processed in reverse order (bottom to top): + +1. **EnforceAuthMiddleware** + + - Handles authentication and authorization + - Configurable public/private endpoints + - OIDC integration + +2. **BuildCql2FilterMiddleware** + + - Builds CQL2 filters based on request context + - Stores filter in request state + +3. **ApplyCql2FilterMiddleware** + + - Retrieves filter from request state + - Applies the built CQL2 filter to requests + - Modifies query strings for GET requests + - Modifies JSON bodies for POST/PUT/PATCH requests + +4. **OpenApiMiddleware** + + - Modifies OpenAPI specification + - Adds security requirements + - Only active if `openapi_spec_endpoint` is configured + +5. **AddProcessTimeHeaderMiddleware** + - Adds processing time headers + - Useful for monitoring/debugging + +### Data filtering via CQL2 + +In order to provide row-level content filtering, the system supports generating CQL2 filters based on request context. These CQL2 filters are then set on outgoing requests prior to the upstream API. + +> [!IMPORTANT] +> The upstream STAC API must support the [STAC API Filter Extension](https://github.com/stac-api-extensions/filter/blob/main/README.md). + +> [!TIP] +> Integration with external authorization systems (e.g. [Open Policy Agent](https://www.openpolicyagent.org/)) can be achieved by replacing the default `BuildCql2FilterMiddleware` with a custom async middleware that is capable of generating [`cql2.Expr` objects](https://developmentseed.org/cql2-rs/latest/python/#cql2.Expr). + +#### Example GET Request Flow + +```mermaid +sequenceDiagram + Client->>Proxy: GET /collections + Note over Proxy: EnforceAuth checks credentials + Note over Proxy: BuildCql2Filter creates filter immediately + Note over Proxy: ApplyCql2Filter modifies query string + Proxy->>STAC API: GET /collection?filter=(collection=landsat) + STAC API->>Client: Response +``` + +#### Filters + +| Supported | Method | Endpoint | Action | Filter | Strategy | +| -------------------------------------------------------- | -------- | ---------------------------------------------- | ------ | ---------- | ------------------------------------------------------------------------------------------------------ | +| ✅ | `POST` | `/search` | Read | Item | Append body with generated CQL2 query. | +| ✅ | `GET` | `/search` | Read | Item | Append query params with generated CQL2 query. | +| ❌ ([#22](https://github.com/developmentseed/issues/22)) | `POST` | `/collections/` | Create | Collection | Validate body with generated CQL2 query. | +| ❌ ([#23](https://github.com/developmentseed/issues/23)) | `GET` | `/collections/{collection_id}` | Read | Collection | Append query params with generated CQL2 query. | +| ❌ ([#22](https://github.com/developmentseed/issues/22)) | `PUT` | `/collections/{collection_id}}` | Update | Collection | Fetch Collection and validate CQL2 query; merge Item with body and validate with generated CQL2 query. | +| ❌ ([#22](https://github.com/developmentseed/issues/22)) | `DELETE` | `/collections/{collection_id}` | Delete | Collection | Fetch Collectiion and validate with CQL2 query. | +| ✅ | `GET` | `/collections/{collection_id}/items` | Read | Item | Append query params with generated CQL2 query. | +| ❌ ([#25](https://github.com/developmentseed/issues/25)) | `GET` | `/collections/{collection_id}/items/{item_id}` | Read | Item | Validate response against CQL2 query. | +| ❌ ([#21](https://github.com/developmentseed/issues/21)) | `POST` | `/collections/{collection_id}/items` | Create | Item | Validate body with generated CQL2 query. | +| ❌ ([#21](https://github.com/developmentseed/issues/21)) | `PUT` | `/collections/{collection_id}/items/{item_id}` | Update | Item | Fetch Item and validate CQL2 query; merge Item with body and validate with generated CQL2 query. | +| ❌ ([#21](https://github.com/developmentseed/issues/21)) | `DELETE` | `/collections/{collection_id}/items/{item_id}` | Delete | Item | Fetch Item and validate with CQL2 query. | +| ❌ ([#21](https://github.com/developmentseed/issues/21)) | `POST` | `/collections/{collection_id}/bulk_items` | Create | Item | Validate items in body with generated CQL2 query. |