|
1 | | -# STAC Auth Proxy |
| 1 | +<div align="center"> |
| 2 | + <h1 style="font-family: monospace">stac auth proxy</h1> |
| 3 | + <p align="center">Reverse proxy to apply auth*n scenarios to STAC APIs.</p> |
| 4 | +</div> |
| 5 | + |
| 6 | +--- |
2 | 7 |
|
3 | 8 | > [!WARNING] |
4 | 9 | > This project is currently in active development and may change drastically in the near future while we work towards solidifying a first release. |
5 | 10 |
|
6 | | -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. |
| 11 | +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. |
7 | 12 |
|
8 | 13 | ## Features |
9 | 14 |
|
10 | | -- 🔐 Selectively apply OIDC auth to some or all endpoints & methods |
11 | | -- 📖 Augments [OpenAPI](https://swagger.io/specification/) with auth information, keeping auto-generated docs (e.g. [Swagger UI](https://swagger.io/tools/swagger-ui/)) accurate |
| 15 | +- 🔐 Authentication: Selectively apply OIDC auth to some or all endpoints & methods |
| 16 | +- 🎟️ Content Filtering: Apply CQL2 filters to client requests, filtering API content based on user context |
| 17 | +- 📖 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 |
12 | 18 |
|
13 | | -### CQL2 Filters |
| 19 | +## Usage |
14 | 20 |
|
15 | | -| Method | Endpoint | Action | Filter | Strategy | |
16 | | -| -------- | ---------------------------------------------- | ------ | ------ | ---------------------------------------------------------------------------------------------------------- | |
17 | | -| `POST` | `/search` | Read | Item | Append body with generated CQL2 query. | |
18 | | -| `GET` | `/search` | Read | Item | Append query params with generated CQL2 query. | |
19 | | -| `GET` | `/collections/{collection_id}/items` | Read | Item | Append query params with generated CQL2 query. | |
20 | | -| `POST` | `/collections/{collection_id}/items` | Create | Item | Validate body with generated CQL2 query. | |
21 | | -| `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. | |
22 | | -| `DELETE` | `/collections/{collection_id}/items/{item_id}` | Delete | Item | Fetch STAC Item and validate with CQL2 query. | |
| 21 | +> [!NOTE] |
| 22 | +> 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)). |
23 | 23 |
|
24 | | -#### Recipes |
| 24 | +### Installation |
25 | 25 |
|
26 | | -Only return collections that are mentioned in a `collections` array encoded within the auth token. |
| 26 | +For local development, his project uses [`uv`](https://docs.astral.sh/uv/) to manage project dependencies and environment. |
27 | 27 |
|
| 28 | +```sh |
| 29 | +uv sync |
28 | 30 | ``` |
29 | | -"A_CONTAINEDBY(id, ('{{ token.collections | join(\"', '\") }}' ))" |
| 31 | + |
| 32 | +Otherwise, the application can be installed as a standard Python module: |
| 33 | + |
| 34 | +```sh |
| 35 | +python3 install src |
30 | 36 | ``` |
31 | 37 |
|
32 | | -## Installation |
| 38 | +### Running |
33 | 39 |
|
34 | | -Set up connection to upstream STAC API and the OpenID Connect provider by setting the following environment variables: |
| 40 | +The simplest way to run the project is by calling the module directly: |
35 | 41 |
|
36 | | -```bash |
37 | | -export STAC_AUTH_PROXY_UPSTREAM_URL="https://some.url" |
38 | | -export STAC_AUTH_PROXY_OIDC_DISCOVERY_URL="https://your-openid-connect-provider.com/.well-known/openid-configuration" |
| 42 | +```sh |
| 43 | +python -m stac_auth_proxy |
39 | 44 | ``` |
40 | 45 |
|
41 | | -Install software: |
| 46 | +Alternatively, the application's factory can be passed to Uvicorn: |
42 | 47 |
|
43 | | -```bash |
44 | | -uv run python -m stac_auth_proxy |
| 48 | +```sh |
| 49 | +uvicorn --factory stac_auth_proxy:create_app |
45 | 50 | ``` |
| 51 | + |
| 52 | +### Configuration |
| 53 | + |
| 54 | +The application is configurable via environment variables. |
| 55 | + |
| 56 | +- `UPSTREAM_URL` |
| 57 | + - The STAC API to proxy requests to |
| 58 | + - **Type:** HTTP(S) URL |
| 59 | + - **Required:** Yes |
| 60 | + - **Example:** `https://your-stac-api.com/stac` |
| 61 | +- `OIDC_DISCOVERY_URL` |
| 62 | + - OpenID Connect discovery document URL |
| 63 | + - **Type:** HTTP(S) URL |
| 64 | + - **Required:** Yes |
| 65 | + - **Example:** `https://auth.example.com/.well-known/openid-configuration` |
| 66 | +- `OIDC_DISCOVERY_INTERNAL_URL` |
| 67 | + - The internal network OpenID Connect discovery document URL |
| 68 | + - **Type:** HTTP(S) URL |
| 69 | + - **Required:** No, defaults to value of `OIDC_DISCOVERY_URL` |
| 70 | + - **Example:** `http://auth/.well-known/openid-configuration` |
| 71 | +- `DEFAULT_PUBLIC` |
| 72 | + - **Description:** Default access policy for endpoints |
| 73 | + - **Type:** boolean |
| 74 | + - **Default:** `false` |
| 75 | + - **Example:** `false`, `1`, `True` |
| 76 | +- `PRIVATE_ENDPOINTS` |
| 77 | + - **Description:** Endpoints explicitely marked as requiring authentication, for use when `DEFAULT_PUBLIC == True` |
| 78 | + - **Type:** JSON object mapping regex patterns to HTTP methods OR to tuples of HTTP methods and an array of strings representing required scopes. |
| 79 | + - **Default:** |
| 80 | + ```json |
| 81 | + { |
| 82 | + "^/collections$": ["POST"], |
| 83 | + "^/collections/([^/]+)$": ["PUT", "PATCH", "DELETE"], |
| 84 | + "^/collections/([^/]+)/items$": ["POST"], |
| 85 | + "^/collections/([^/]+)/items/([^/]+)$": ["PUT", "PATCH", "DELETE"], |
| 86 | + "^/collections/([^/]+)/bulk_items$": ["POST"] |
| 87 | + } |
| 88 | + ``` |
| 89 | +- `PUBLIC_ENDPOINTS` |
| 90 | + - **Description:** Endpoints explicitely marked as not requiring authentication, for use when `DEFAULT_PUBLIC == False` |
| 91 | + - **Type:** JSON object mapping regex patterns to HTTP methods |
| 92 | + - **Default:** |
| 93 | + ```json |
| 94 | + { |
| 95 | + "^/api.html$": ["GET"], |
| 96 | + "^/api$": ["GET"] |
| 97 | + } |
| 98 | + ``` |
| 99 | +- `OPENAPI_SPEC_ENDPOINT` |
| 100 | + - Path to serve OpenAPI specification |
| 101 | + - **Type:** string or null |
| 102 | + - **Default:** `null` (disabled) |
| 103 | + - **Example:** `/api` |
| 104 | +- `ITEMS_FILTER` |
| 105 | + - Configuration for item-level filtering |
| 106 | + - **Type:** JSON object with class configuration |
| 107 | + - **Default:** `null` |
| 108 | + - Components: |
| 109 | + - `cls`: Python import path |
| 110 | + - `args`: List of positional arguments |
| 111 | + - `kwargs`: Dictionary of keyword arguments |
| 112 | + - **Example:** |
| 113 | + ```json |
| 114 | + { |
| 115 | + "cls": "my_package.filters.OrganizationFilter", |
| 116 | + "args": ["org1"], |
| 117 | + "kwargs": { |
| 118 | + "field_name": "properties.organization" |
| 119 | + } |
| 120 | + } |
| 121 | + ``` |
| 122 | +- `ITEMS_FILTER_ENDPOINTS` |
| 123 | + - Where to apply item filtering |
| 124 | + - **Type:** JSON object mapping regex patterns to HTTP methods |
| 125 | + - **Default:** |
| 126 | + ```json |
| 127 | + { |
| 128 | + "^/search$": ["POST"], |
| 129 | + "^/collections/([^/]+)/items$": ["GET", "POST"] |
| 130 | + } |
| 131 | + ``` |
| 132 | + |
| 133 | +## Architecture |
| 134 | + |
| 135 | +### Middleware Stack |
| 136 | + |
| 137 | +The middleware stack is processed in reverse order (bottom to top): |
| 138 | + |
| 139 | +1. **EnforceAuthMiddleware** |
| 140 | + |
| 141 | + - Handles authentication and authorization |
| 142 | + - Configurable public/private endpoints |
| 143 | + - OIDC integration |
| 144 | + |
| 145 | +2. **BuildCql2FilterMiddleware** |
| 146 | + |
| 147 | + - Builds CQL2 filters based on request context |
| 148 | + - Stores filter in request state |
| 149 | + |
| 150 | +3. **ApplyCql2FilterMiddleware** |
| 151 | + |
| 152 | + - Retrieves filter from request state |
| 153 | + - Applies the built CQL2 filter to requests |
| 154 | + - Modifies query strings for GET requests |
| 155 | + - Modifies JSON bodies for POST/PUT/PATCH requests |
| 156 | + |
| 157 | +4. **OpenApiMiddleware** |
| 158 | + |
| 159 | + - Modifies OpenAPI specification |
| 160 | + - Adds security requirements |
| 161 | + - Only active if `openapi_spec_endpoint` is configured |
| 162 | + |
| 163 | +5. **AddProcessTimeHeaderMiddleware** |
| 164 | + - Adds processing time headers |
| 165 | + - Useful for monitoring/debugging |
| 166 | + |
| 167 | +### Data filtering via CQL2 |
| 168 | + |
| 169 | +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. |
| 170 | + |
| 171 | +> [!IMPORTANT] |
| 172 | +> The upstream STAC API must support the [STAC API Filter Extension](https://github.com/stac-api-extensions/filter/blob/main/README.md). |
| 173 | + |
| 174 | +> [!TIP] |
| 175 | +> 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). |
| 176 | + |
| 177 | +#### Example GET Request Flow |
| 178 | + |
| 179 | +```mermaid |
| 180 | +sequenceDiagram |
| 181 | + Client->>Proxy: GET /collections |
| 182 | + Note over Proxy: EnforceAuth checks credentials |
| 183 | + Note over Proxy: BuildCql2Filter creates filter immediately |
| 184 | + Note over Proxy: ApplyCql2Filter modifies query string |
| 185 | + Proxy->>STAC API: GET /collection?filter=(collection=landsat) |
| 186 | + STAC API->>Client: Response |
| 187 | +``` |
| 188 | + |
| 189 | +#### Filters |
| 190 | + |
| 191 | +| Supported | Method | Endpoint | Action | Filter | Strategy | |
| 192 | +| -------------------------------------------------------- | -------- | ---------------------------------------------- | ------ | ---------- | ------------------------------------------------------------------------------------------------------ | |
| 193 | +| ✅ | `POST` | `/search` | Read | Item | Append body with generated CQL2 query. | |
| 194 | +| ✅ | `GET` | `/search` | Read | Item | Append query params with generated CQL2 query. | |
| 195 | +| ❌ ([#22](https://github.com/developmentseed/issues/22)) | `POST` | `/collections/` | Create | Collection | Validate body with generated CQL2 query. | |
| 196 | +| ❌ ([#23](https://github.com/developmentseed/issues/23)) | `GET` | `/collections/{collection_id}` | Read | Collection | Append query params with generated CQL2 query. | |
| 197 | +| ❌ ([#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. | |
| 198 | +| ❌ ([#22](https://github.com/developmentseed/issues/22)) | `DELETE` | `/collections/{collection_id}` | Delete | Collection | Fetch Collectiion and validate with CQL2 query. | |
| 199 | +| ✅ | `GET` | `/collections/{collection_id}/items` | Read | Item | Append query params with generated CQL2 query. | |
| 200 | +| ❌ ([#25](https://github.com/developmentseed/issues/25)) | `GET` | `/collections/{collection_id}/items/{item_id}` | Read | Item | Validate response against CQL2 query. | |
| 201 | +| ❌ ([#21](https://github.com/developmentseed/issues/21)) | `POST` | `/collections/{collection_id}/items` | Create | Item | Validate body with generated CQL2 query. | |
| 202 | +| ❌ ([#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. | |
| 203 | +| ❌ ([#21](https://github.com/developmentseed/issues/21)) | `DELETE` | `/collections/{collection_id}/items/{item_id}` | Delete | Item | Fetch Item and validate with CQL2 query. | |
| 204 | +| ❌ ([#21](https://github.com/developmentseed/issues/21)) | `POST` | `/collections/{collection_id}/bulk_items` | Create | Item | Validate items in body with generated CQL2 query. | |
0 commit comments