Skip to content

Commit fdfa62c

Browse files
committed
Merge branch 'main' into authentication-ext/asset-signing
2 parents 66a03ad + 9c51ce0 commit fdfa62c

File tree

17 files changed

+713
-347
lines changed

17 files changed

+713
-347
lines changed

Dockerfile

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
1+
# https://github.com/astral-sh/uv-docker-example/blob/c16a61fb3e6ab568ac58d94b73a7d79594a5d570/Dockerfile
12
FROM python:3.13-slim
23

3-
EXPOSE 8000
4-
54
WORKDIR /app
65

7-
RUN apt-get update && apt-get install -y gcc libpq-dev
6+
ENV PATH="/app/.venv/bin:$PATH"
7+
ENV UV_COMPILE_BYTECODE=1
8+
ENV UV_LINK_MODE=copy
9+
ENV UV_PROJECT_ENVIRONMENT=/usr/local
810

911
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
1012

11-
COPY . .
12-
13-
ENV PYTHONUNBUFFERED=1
13+
# Install the project's dependencies using the lockfile and settings
14+
RUN --mount=type=cache,target=/root/.cache/uv \
15+
--mount=type=bind,source=uv.lock,target=uv.lock \
16+
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
17+
uv sync --frozen --no-install-project --no-dev
1418

15-
RUN uv sync --no-dev --locked
19+
# Then, add the rest of the project source code and install it
20+
# Installing separately from its dependencies allows optimal layer caching
21+
ADD . /app
22+
RUN --mount=type=cache,target=/root/.cache/uv \
23+
uv sync --frozen --no-dev
1624

17-
CMD ["uv", "run", "--locked", "--no-dev", "python", "-m", "stac_auth_proxy"]
25+
CMD ["python", "-m", "stac_auth_proxy"]

README.md

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pip install -e .
3333
```
3434

3535
> [!NOTE]
36-
> This project will be available on PyPi in the near future ([#30](https://github.com/developmentseed/stac-auth-proxy/issues/30)).
36+
> This project will be available on PyPi in the near future[^30].
3737
3838
### Running
3939

@@ -43,8 +43,8 @@ The simplest way to run the project is by invoking the application via Docker:
4343
docker run \
4444
-it --rm \
4545
-p 8000:8000 \
46-
-e UPSTREAM_URL=https://google.com \
47-
-e OIDC_DISCOVERY_URL=https://auth.openveda.cloud/realms/veda/.well-known/openid-configuration \
46+
-e UPSTREAM_URL=https://my-stac-api \
47+
-e OIDC_DISCOVERY_URL=https://my-auth-server/.well-known/openid-configuration \
4848
ghcr.io/developmentseed/stac-auth-proxy:latest
4949
```
5050

@@ -71,6 +71,10 @@ The application is configurable via environment variables.
7171
- **Type:** boolean
7272
- **Required:** No, defaults to `true`
7373
- **Example:** `false`, `1`, `True`
74+
- **`CHECK_CONFORMANCE`**, ensure upstream API conforms to required conformance classes before starting proxy
75+
- **Type:** boolean
76+
- **Required:** No, defaults to `true`
77+
- **Example:** `false`, `1`, `True`
7478
- **`HEALTHZ_PREFIX`**, path prefix for health check endpoints
7579
- **Type:** string
7680
- **Required:** No, defaults to `/healthz`
@@ -105,26 +109,26 @@ The application is configurable via environment variables.
105109
- **Required:** No, defaults to the following:
106110
```json
107111
{
108-
r"^/api.html$": ["GET"],
109-
r"^/api$": ["GET"],
110-
r"^/docs/oauth2-redirect": ["GET"],
111-
r"^/healthz": ["GET"],
112+
"^/api.html$": ["GET"],
113+
"^/api$": ["GET"],
114+
"^/docs/oauth2-redirect": ["GET"],
115+
"^/healthz": ["GET"]
112116
}
113117
```
114118
- **`OPENAPI_SPEC_ENDPOINT`**, path of OpenAPI specification, used for augmenting spec response with auth configuration
115119
- **Type:** string or null
116120
- **Required:** No, defaults to `null` (disabled)
117121
- **Example:** `/api`
118122
- Filtering
119-
- **`ITEMS_FILTER_CLS`**, [cql2 expression](https://developmentseed.org/cql2-rs/latest/python/#cql2.Expr) generator for item-level filtering
123+
- **`ITEMS_FILTER_CLS`**, CQL2 expression generator for item-level filtering
120124
- **Type:** JSON object with class configuration
121125
- **Required:** No, defaults to `null` (disabled)
122126
- **Example:** `my_package.filters:OrganizationFilter`
123-
- **`ITEMS_FILTER_ARGS`**, [cql2 expression](https://developmentseed.org/cql2-rs/latest/python/#cql2.Expr) generator for item-level filtering
127+
- **`ITEMS_FILTER_ARGS`**, Positional arguments for CQL2 expression generator
124128
- **Type:** List of positional arguments used to initialize the class
125129
- **Required:** No, defaults to `[]`
126130
- **Example:**: `["org1"]`
127-
- **`ITEMS_FILTER_KWARGS`**, [cql2 expression](https://developmentseed.org/cql2-rs/latest/python/#cql2.Expr) generator for item-level filtering
131+
- **`ITEMS_FILTER_KWARGS`**, Keyword arguments for CQL2 expression generator
128132
- **Type:** Dictionary of keyword arguments used to initialize the class
129133
- **Required:** No, defaults to `{}`
130134
- **Example:** `{ "field_name": "properties.organization" }`
@@ -172,7 +176,7 @@ The majority of the proxy's functionality occurs within a chain of middlewares.
172176
The system supports generating CQL2 filters based on request context to provide row-level content filtering. These CQL2 filters are then set on outgoing requests prior to the upstream API.
173177

174178
> [!IMPORTANT]
175-
> The upstream STAC API must support the [STAC API Filter Extension](https://github.com/stac-api-extensions/filter/blob/main/README.md), including the [Features Filter](http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter) conformance class on to the Features resource (`/collections/{cid}/items`) [#37](https://github.com/developmentseed/stac-auth-proxy/issues/37).
179+
> The upstream STAC API must support the [STAC API Filter Extension](https://github.com/stac-api-extensions/filter/blob/main/README.md), including the [Features Filter](http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter) conformance class on to the Features resource (`/collections/{cid}/items`)[^37].
176180

177181
> [!TIP]
178182
> Integration with external authorization systems (e.g. [Open Policy Agent](https://www.openpolicyagent.org/)) can be achieved by specifying an `ITEMS_FILTER` that points to a class/function that, once initialized, returns a [`cql2.Expr` object](https://developmentseed.org/cql2-rs/latest/python/#cql2.Expr) when called with the request context.
@@ -191,53 +195,58 @@ If enabled, filters are intended to be applied to the following endpoints:
191195
- **Action:** Read Item
192196
- **Applied Filter:** `ITEMS_FILTER`
193197
- **Strategy:** Append body with generated CQL2 query.
194-
- `GET /collections/{collection_id}`
195-
- **Supported:** ❌ ([#23](https://github.com/developmentseed/stac-auth-proxy/issues/23))
196-
- **Action:** Read Collection
197-
- **Applied Filter:** `COLLECTIONS_FILTER`
198-
- **Strategy:** Append query params with generated CQL2 query.
199198
- `GET /collections/{collection_id}/items`
200199
- **Supported:** ✅
201200
- **Action:** Read Item
202201
- **Applied Filter:** `ITEMS_FILTER`
203202
- **Strategy:** Append query params with generated CQL2 query.
204203
- `GET /collections/{collection_id}/items/{item_id}`
205-
- **Supported:** ❌ ([#25](https://github.com/developmentseed/stac-auth-proxy/issues/25))
204+
- **Supported:**
206205
- **Action:** Read Item
207206
- **Applied Filter:** `ITEMS_FILTER`
208207
- **Strategy:** Validate response against CQL2 query.
208+
- `GET /collections`
209+
- **Supported:** ❌[^23]
210+
- **Action:** Read Collection
211+
- **Applied Filter:** `COLLECTIONS_FILTER`
212+
- **Strategy:** Append query params with generated CQL2 query.
213+
- `GET /collections/{collection_id}`
214+
- **Supported:** ❌[^23]
215+
- **Action:** Read Collection
216+
- **Applied Filter:** `COLLECTIONS_FILTER`
217+
- **Strategy:** Validate response against CQL2 query.
209218
- `POST /collections/`
210-
- **Supported:** ❌ ([#22](https://github.com/developmentseed/stac-auth-proxy/issues/22))
219+
- **Supported:** ❌[^22]
211220
- **Action:** Create Collection
212221
- **Applied Filter:** `COLLECTIONS_FILTER`
213222
- **Strategy:** Validate body with generated CQL2 query.
214223
- `PUT /collections/{collection_id}}`
215-
- **Supported:** ❌ ([#22](https://github.com/developmentseed/stac-auth-proxy/issues/22))
224+
- **Supported:** ❌[^22]
216225
- **Action:** Update Collection
217226
- **Applied Filter:** `COLLECTIONS_FILTER`
218227
- **Strategy:** Fetch Collection and validate CQL2 query; merge Item with body and validate with generated CQL2 query.
219228
- `DELETE /collections/{collection_id}`
220-
- **Supported:** ❌ ([#22](https://github.com/developmentseed/stac-auth-proxy/issues/22))
229+
- **Supported:** ❌[^22]
221230
- **Action:** Delete Collection
222231
- **Applied Filter:** `COLLECTIONS_FILTER`
223232
- **Strategy:** Fetch Collectiion and validate with CQL2 query.
224233
- `POST /collections/{collection_id}/items`
225-
- **Supported:** ❌ ([#21](https://github.com/developmentseed/stac-auth-proxy/issues/21))
234+
- **Supported:** ❌[^21]
226235
- **Action:** Create Item
227236
- **Applied Filter:** `ITEMS_FILTER`
228237
- **Strategy:** Validate body with generated CQL2 query.
229238
- `PUT /collections/{collection_id}/items/{item_id}`
230-
- **Supported:** ❌ ([#21](https://github.com/developmentseed/stac-auth-proxy/issues/21))
239+
- **Supported:** ❌[^21]
231240
- **Action:** Update Item
232241
- **Applied Filter:** `ITEMS_FILTER`
233242
- **Strategy:** Fetch Item and validate CQL2 query; merge Item with body and validate with generated CQL2 query.
234243
- `DELETE /collections/{collection_id}/items/{item_id}`
235-
- **Supported:** ❌ ([#21](https://github.com/developmentseed/stac-auth-proxy/issues/21))
244+
- **Supported:** ❌[^21]
236245
- **Action:** Delete Item
237246
- **Applied Filter:** `ITEMS_FILTER`
238247
- **Strategy:** Fetch Item and validate with CQL2 query.
239248
- `POST /collections/{collection_id}/bulk_items`
240-
- **Supported:** ❌ ([#21](https://github.com/developmentseed/stac-auth-proxy/issues/21))
249+
- **Supported:** ❌[^21]
241250
- **Action:** Create Items
242251
- **Applied Filter:** `ITEMS_FILTER`
243252
- **Strategy:** Validate items in body with generated CQL2 query.
@@ -253,3 +262,9 @@ sequenceDiagram
253262
Proxy->>STAC API: GET /collection?filter=(collection=landsat)
254263
STAC API->>Client: Response
255264
```
265+
266+
[^21]: https://github.com/developmentseed/stac-auth-proxy/issues/21
267+
[^22]: https://github.com/developmentseed/stac-auth-proxy/issues/22
268+
[^23]: https://github.com/developmentseed/stac-auth-proxy/issues/23
269+
[^30]: https://github.com/developmentseed/stac-auth-proxy/issues/30
270+
[^37]: https://github.com/developmentseed/stac-auth-proxy/issues/37

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ classifiers = [
88
dependencies = [
99
"boto3>=1.37.16",
1010
"brotli>=1.1.0",
11-
"cql2>=0.3.5",
11+
"cql2>=0.3.6",
1212
"fastapi>=0.115.5",
1313
"httpx[http2]>=0.28.0",
1414
"jinja2>=3.1.4",

src/stac_auth_proxy/app.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
EnforceAuthMiddleware,
2323
OpenApiMiddleware,
2424
)
25-
from .utils.lifespan import check_server_health
25+
from .utils.lifespan import check_conformance, check_server_health
2626

2727
logger = logging.getLogger(__name__)
2828

@@ -41,8 +41,26 @@ async def lifespan(app: FastAPI):
4141

4242
# Wait for upstream servers to become available
4343
if settings.wait_for_upstream:
44-
for url in [settings.upstream_url, settings.oidc_discovery_internal_url]:
44+
logger.info("Running upstream server health checks...")
45+
urls = [settings.upstream_url, settings.oidc_discovery_internal_url]
46+
for url in urls:
4547
await check_server_health(url=url)
48+
logger.info(
49+
"Upstream servers are healthy:\n%s",
50+
"\n".join([f" - {url}" for url in urls]),
51+
)
52+
53+
# Log all middleware connected to the app
54+
logger.info(
55+
"Connected middleware:\n%s",
56+
"\n".join([f" - {m.cls.__name__}" for m in app.user_middleware]),
57+
)
58+
59+
if settings.check_conformance:
60+
await check_conformance(
61+
app.user_middleware,
62+
str(settings.upstream_url),
63+
)
4664

4765
yield
4866

@@ -107,19 +125,19 @@ async def lifespan(app: FastAPI):
107125
)
108126

109127
app.add_middleware(
110-
EnforceAuthMiddleware,
111-
public_endpoints=settings.public_endpoints,
112-
private_endpoints=settings.private_endpoints,
113-
default_public=settings.default_public,
114-
oidc_config_url=settings.oidc_discovery_internal_url,
128+
CompressionMiddleware,
115129
)
116130

117131
app.add_middleware(
118-
CompressionMiddleware,
132+
AddProcessTimeHeaderMiddleware,
119133
)
120134

121135
app.add_middleware(
122-
AddProcessTimeHeaderMiddleware,
136+
EnforceAuthMiddleware,
137+
public_endpoints=settings.public_endpoints,
138+
private_endpoints=settings.private_endpoints,
139+
default_public=settings.default_public,
140+
oidc_config_url=settings.oidc_discovery_internal_url,
123141
)
124142

125143
return app

src/stac_auth_proxy/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class Settings(BaseSettings):
3939
oidc_discovery_internal_url: HttpUrl
4040

4141
wait_for_upstream: bool = True
42+
check_conformance: bool = True
4243

4344
# Endpoints
4445
healthz_prefix: str = Field(pattern=_PREFIX_PATTERN, default="/healthz")

src/stac_auth_proxy/filters/template.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from dataclasses import dataclass, field
44
from typing import Any
55

6-
from cql2 import Expr
76
from jinja2 import BaseLoader, Environment
87

98

@@ -18,10 +17,6 @@ def __post_init__(self):
1817
"""Initialize the Jinja2 environment."""
1918
self.env = Environment(loader=BaseLoader).from_string(self.template_str)
2019

21-
async def __call__(self, context: dict[str, Any]) -> Expr:
20+
async def __call__(self, context: dict[str, Any]) -> str:
2221
"""Render a CQL2 filter expression with the request and auth token."""
23-
# TODO: How to handle the case where auth_token is null?
24-
cql2_str = self.env.render(**context).strip()
25-
cql2_expr = Expr(cql2_str)
26-
cql2_expr.validate()
27-
return cql2_expr
22+
return self.env.render(**context).strip()

0 commit comments

Comments
 (0)