Skip to content

Commit 3d24e08

Browse files
committed
feat(auth-tests): Add CLI tool for testing filter factories
1 parent b7c6ea6 commit 3d24e08

File tree

8 files changed

+1524
-0
lines changed

8 files changed

+1524
-0
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ repos:
2626
additional_dependencies:
2727
- types-simplejson
2828
- types-attrs
29+
- types-PyYAML
2930
- pydantic~=2.0
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
# Testing Auth Rules
2+
3+
The `stac-auth-tests` CLI tool validates your custom filter classes by running them in the same environment as production, ensuring your authorization rules work as expected.
4+
5+
## Overview
6+
7+
Run sanity checks on your auth filters within your production environment (Docker containers, kubernetes pods, etc.) to:
8+
9+
- **Validate filter logic** - Ensure filters generate correct CQL2 expressions and match items as expected
10+
- **Catch regressions** - Verify changes don't break expected behavior before deployment
11+
- **Test with production data** - Run filters against real STAC APIs and databases in your stack
12+
13+
## Test File Format
14+
15+
Test files use YAML (recommended) or JSON. Each test case contains:
16+
- **context** - Request and JWT payload passed to your filter
17+
- **tests** - Tuples of `[item, expected_match]` to validate
18+
19+
### Example Test File
20+
21+
```yaml
22+
# tests/auth_rules.yaml
23+
# Define reusable items with YAML anchors
24+
items:
25+
public_item: &public_item
26+
id: item-1
27+
type: Feature
28+
collection: my-collection
29+
properties:
30+
private: false
31+
geometry: null
32+
33+
private_item: &private_item
34+
id: item-2
35+
type: Feature
36+
collection: my-collection
37+
properties:
38+
private: true
39+
geometry: null
40+
41+
# Define reusable contexts
42+
contexts:
43+
anonymous: &anonymous
44+
req:
45+
path: /collections/my-collection/items
46+
method: GET
47+
headers: {}
48+
query_params: {}
49+
path_params: {}
50+
51+
authenticated: &authenticated
52+
req:
53+
path: /collections/my-collection/items
54+
method: GET
55+
headers:
56+
authorization: Bearer token
57+
query_params: {}
58+
path_params: {}
59+
payload:
60+
sub: user123
61+
collections: ["my-collection"]
62+
63+
test_cases:
64+
- name: Anonymous users see only public items
65+
context: *anonymous
66+
tests:
67+
- [*public_item, true]
68+
- [*private_item, false]
69+
70+
- name: Authenticated users see their collections
71+
context: *authenticated
72+
tests:
73+
- [*public_item, true]
74+
- [*private_item, true]
75+
```
76+
77+
See `tests/example_auth_rules.yaml` for a complete example.
78+
79+
## Running Tests
80+
81+
### With Docker Compose (Local Development)
82+
83+
Add test files to your compose volumes:
84+
85+
```yaml
86+
# docker-compose.yaml
87+
services:
88+
proxy:
89+
volumes:
90+
- ./tests:/app/tests
91+
```
92+
93+
Run tests in your stack:
94+
95+
```bash
96+
# Start services
97+
docker compose up -d
98+
99+
# Run tests (creates isolated container with access to your stack)
100+
docker compose run --rm proxy stac-auth-tests \
101+
--filter-class "my_filters:ItemsFilter" \
102+
--test-file /app/tests/auth_rules.yaml
103+
```
104+
105+
This approach:
106+
- Tests against your actual upstream STAC API
107+
- Runs filters that make API calls (e.g., fetching public collections)
108+
- Uses the same environment variables and network as production
109+
110+
### In Production Containers
111+
112+
```dockerfile
113+
FROM ghcr.io/developmentseed/stac-auth-proxy:latest
114+
COPY ./my_filters.py /app/my_filters.py
115+
COPY ./tests /app/tests
116+
```
117+
118+
```bash
119+
# Build and test
120+
docker build -t my-stac-proxy .
121+
docker run --rm \
122+
-e UPSTREAM_URL=http://stac-api:8080 \
123+
my-stac-proxy \
124+
stac-auth-tests \
125+
--filter-class "my_filters:ItemsFilter" \
126+
--test-file /app/tests/auth_rules.yaml
127+
```
128+
129+
### Locally (Development)
130+
131+
```bash
132+
pip install -e .
133+
134+
stac-auth-tests \
135+
--filter-class "stac_auth_proxy.filters:Template" \
136+
--filter-args '["(properties.private = false)"]' \
137+
--test-file tests/auth_rules.yaml
138+
```
139+
140+
## CLI Options
141+
142+
```bash
143+
stac-auth-tests \
144+
--filter-class "module.path:ClassName" # Required: filter class to test
145+
--filter-args '[...]' # Optional: JSON array of positional args
146+
--filter-kwargs '{...}' # Optional: JSON object of keyword args
147+
--test-file path/to/tests.yaml # Required: test file path
148+
```
149+
150+
## Example: Testing Custom Filter
151+
152+
```python
153+
# my_filters.py
154+
import dataclasses
155+
from typing import Any
156+
157+
@dataclasses.dataclass
158+
class ItemsFilter:
159+
collections_claim: str = "collections"
160+
161+
async def __call__(self, context: dict[str, Any]) -> str:
162+
jwt = context.get("payload")
163+
if jwt:
164+
collections = jwt.get(self.collections_claim, [])
165+
return f"collection IN ({','.join(repr(c) for c in collections)})"
166+
return "(private IS NULL OR private = false)"
167+
```
168+
169+
```yaml
170+
# tests/my_tests.yaml
171+
items:
172+
allowed: &allowed
173+
id: item-1
174+
collection: allowed-col
175+
type: Feature
176+
properties: {}
177+
geometry: null
178+
179+
forbidden: &forbidden
180+
id: item-2
181+
collection: forbidden-col
182+
type: Feature
183+
properties: {}
184+
geometry: null
185+
186+
test_cases:
187+
- name: User with collection access
188+
context:
189+
req:
190+
path: /search
191+
method: POST
192+
headers:
193+
authorization: Bearer token
194+
query_params: {}
195+
path_params: {}
196+
payload:
197+
sub: user123
198+
collections: ["allowed-col"]
199+
tests:
200+
- [*allowed, true]
201+
- [*forbidden, false]
202+
```
203+
204+
```bash
205+
# Test it
206+
docker compose run --rm proxy stac-auth-tests \
207+
--filter-class "my_filters:ItemsFilter" \
208+
--test-file /app/tests/my_tests.yaml
209+
```
210+
211+
## CI/CD Integration
212+
213+
```yaml
214+
# .github/workflows/test.yml
215+
name: Test Auth Rules
216+
217+
on: [push, pull_request]
218+
219+
jobs:
220+
test:
221+
runs-on: ubuntu-latest
222+
steps:
223+
- uses: actions/checkout@v3
224+
- name: Build image
225+
run: docker build -t test-image .
226+
- name: Test auth rules
227+
run: |
228+
docker run --rm test-image \
229+
stac-auth-tests \
230+
--filter-class "my_filters:ItemsFilter" \
231+
--test-file /app/tests/auth_rules.yaml
232+
```
233+
234+
## Troubleshooting
235+
236+
**"Failed to generate or validate CQL2 filter"**
237+
- Your filter returned invalid CQL2 syntax
238+
- Check that property references and operators are correct
239+
240+
**"Item match failures"**
241+
- Filter is valid but items don't match as expected
242+
- Verify property paths (e.g., `properties.private` vs `private`)
243+
- Check data types match (strings vs booleans)
244+
245+
**"Error loading filter class"**
246+
- Check class path format: `module.path:ClassName`
247+
- Verify module is in Python path and dependencies are installed
248+
249+
## See Also
250+
251+
- [Record-Level Authorization Guide](record-level-auth.md)
252+
- [CQL2 Specification](https://docs.ogc.org/DRAFTS/21-065.html)

pyproject.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies = [
1515
"jinja2>=3.1.4",
1616
"pydantic-settings>=2.6.1",
1717
"pyjwt>=2.10.1",
18+
"pyyaml>=6.0",
1819
"starlette-cramjam>=0.4.0",
1920
"uvicorn>=0.32.1",
2021
]
@@ -26,6 +27,9 @@ readme = "README.md"
2627
requires-python = ">=3.9"
2728
version = "0.10.0"
2829

30+
[project.scripts]
31+
stac-auth-tests = "stac_auth_proxy.cli.test_auth_rules:main"
32+
2933
[project.optional-dependencies]
3034
docs = [
3135
"griffe-fieldz>=0.3.0",
@@ -97,6 +101,7 @@ dev = [
97101
"pytest>=8.3.3",
98102
"ruff>=0.0.238",
99103
"starlette-cramjam>=0.4.0",
104+
"types-PyYAML",
100105
"types-simplejson",
101106
"types-attrs",
102107
]
@@ -124,3 +129,11 @@ markers = [
124129
"integration: marks tests as integration tests",
125130
"unit: marks tests as unit tests",
126131
]
132+
133+
[tool.uv.workspace]
134+
members = [
135+
".",
136+
]
137+
138+
[tool.uv.sources]
139+
stac-auth-proxy = { workspace = true }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""CLI tools for stac-auth-proxy."""

0 commit comments

Comments
 (0)