|
| 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) |
0 commit comments