Skip to content

Commit e2996aa

Browse files
committed
Added custom filter logic for stac-auth-proxy.
1 parent 0cf94ad commit e2996aa

7 files changed

Lines changed: 304 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Added support for annotations on the PgSTAC bootstrap job via `pgstacBootstrap.jobAnnotations` in values.yaml [#381](https://github.com/developmentseed/eoapi-k8s/pull/381)
1313
- Added load testing scripts [#373](https://github.com/developmentseed/eoapi-k8s/pull/373)
1414
- Added auth support to STAC Browser [#376](https://github.com/developmentseed/eoapi-k8s/pull/376)
15+
- Added support for custom filters configuration via `customFiltersFile` in values.yaml [#388](https://github.com/developmentseed/eoapi-k8s/pull/388)
1516

1617
### Fixed
1718

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""
2+
Sample custom filters for STAC Auth Proxy.
3+
This file demonstrates the structure needed for custom collection and item filters.
4+
"""
5+
6+
import dataclasses
7+
from typing import Any
8+
9+
10+
@dataclasses.dataclass
11+
class CollectionsFilter:
12+
"""Returns CQL2 filter for /collections endpoint."""
13+
14+
async def __call__(self, context: dict[str, Any]) -> str | dict[str, Any]:
15+
"""
16+
Return format:
17+
- CQL2-text string: "1=1" or "private = false"
18+
- CQL2-JSON dict: {"op": "=", "args": [{"property": "owner"}, "user123"]}
19+
20+
Examples:
21+
- Allow all: return "1=1"
22+
- User-specific: return f"owner = '{context['token']['sub']}'"
23+
- Public only: return "private = false" if not context["token"] else "1=1"
24+
- Complex: return {"op": "in", "args": [{"property": "id"}, ["col1", "col2"]]}
25+
"""
26+
return "1=1"
27+
28+
29+
@dataclasses.dataclass
30+
class ItemsFilter:
31+
"""Returns CQL2 filter for /search and /collections/{id}/items endpoints."""
32+
33+
async def __call__(self, context: dict[str, Any]) -> str | dict[str, Any]:
34+
"""
35+
Examples:
36+
- Allow all: return "1=1"
37+
- Collection-based: return f"collection = '{context['collection_id']}'"
38+
- User-specific: return f"properties.owner = '{context['token']['sub']}'"
39+
- Complex: return {"op": "in", "args": [{"property": "collection"}, approved_list]}
40+
"""
41+
return "1=1"

charts/eoapi/profiles/experimental.yaml

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,8 +369,25 @@ ingress:
369369
stac-auth-proxy:
370370
enabled: true
371371
# For testing this will be set dynamically; for production, point to your OIDC server
372-
# env:
373-
# OIDC_DISCOVERY_URL: "http://eoapi-mock-oidc-server.eoapi.svc.cluster.local:8080/.well-known/openid-configuration"
372+
env:
373+
# OIDC_DISCOVERY_URL: "http://eoapi-mock-oidc-server.eoapi.svc.cluster.local:8080/.well-known/openid-configuration"
374+
# Custom filter classes
375+
COLLECTIONS_FILTER_CLS: "stac_auth_proxy.custom_filters:CollectionsFilter"
376+
ITEMS_FILTER_CLS: "stac_auth_proxy.custom_filters:ItemsFilter"
377+
378+
# Custom filters configuration
379+
customFiltersFile: "data/stac-auth-proxy/custom_filters.py"
380+
381+
extraVolumes:
382+
- name: filters
383+
configMap:
384+
name: eoapi-stac-auth-proxy-custom-filters
385+
386+
extraVolumeMounts:
387+
- name: filters
388+
mountPath: /app/src/stac_auth_proxy/custom_filters.py
389+
subPath: custom_filters.py
390+
readOnly: true
374391

375392
######################
376393
# MOCK OIDC SERVER
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{{- if index .Values "stac-auth-proxy" "enabled" }}
2+
{{- $stacAuthProxy := index .Values "stac-auth-proxy" }}
3+
{{- if and (hasKey $stacAuthProxy "extraVolumes") $stacAuthProxy.extraVolumes }}
4+
{{- $filterFile := $stacAuthProxy.customFiltersFile | default "data/stac-auth-proxy/custom_filters.py" }}
5+
apiVersion: v1
6+
kind: ConfigMap
7+
metadata:
8+
name: eoapi-stac-auth-proxy-custom-filters
9+
labels:
10+
{{- include "eoapi.labels" . | nindent 4 }}
11+
app.kubernetes.io/component: stac-auth-proxy
12+
data:
13+
custom_filters.py: |
14+
{{ .Files.Get $filterFile | indent 4 }}
15+
{{- end }}
16+
{{- end }}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
suite: test stac-auth-proxy custom filters ConfigMap
2+
templates:
3+
- templates/_helpers/core.tpl
4+
- templates/core/stac-auth-proxy-filters-configmap.yaml
5+
6+
tests:
7+
- it: should create ConfigMap when stac-auth-proxy is enabled and extraVolumes is defined
8+
set:
9+
stac-auth-proxy.enabled: true
10+
stac-auth-proxy.extraVolumes:
11+
- name: filters
12+
configMap:
13+
name: test-filters
14+
template: templates/core/stac-auth-proxy-filters-configmap.yaml
15+
asserts:
16+
- isKind:
17+
of: ConfigMap
18+
- equal:
19+
path: metadata.name
20+
value: eoapi-stac-auth-proxy-custom-filters
21+
- isNotEmpty:
22+
path: data
23+
24+
- it: should not create ConfigMap when stac-auth-proxy is disabled
25+
set:
26+
stac-auth-proxy.enabled: false
27+
stac-auth-proxy.extraVolumes:
28+
- name: filters
29+
configMap:
30+
name: test-filters
31+
asserts:
32+
- hasDocuments:
33+
count: 0
34+
35+
- it: should not create ConfigMap when extraVolumes is not defined
36+
set:
37+
stac-auth-proxy.enabled: true
38+
asserts:
39+
- hasDocuments:
40+
count: 0
41+
42+
- it: should have correct labels
43+
set:
44+
stac-auth-proxy.enabled: true
45+
stac-auth-proxy.extraVolumes:
46+
- name: filters
47+
configMap:
48+
name: test-filters
49+
template: templates/core/stac-auth-proxy-filters-configmap.yaml
50+
asserts:
51+
- equal:
52+
path: metadata.labels["app.kubernetes.io/component"]
53+
value: stac-auth-proxy
54+
- exists:
55+
path: metadata.labels["app.kubernetes.io/name"]
56+
- exists:
57+
path: metadata.labels["app.kubernetes.io/instance"]
58+
- exists:
59+
path: metadata.labels["helm.sh/chart"]
60+
61+
- it: should use custom file path when customFiltersFile is specified
62+
set:
63+
stac-auth-proxy.enabled: true
64+
stac-auth-proxy.customFiltersFile: "data/eoepca_filters.py"
65+
stac-auth-proxy.extraVolumes:
66+
- name: filters
67+
configMap:
68+
name: test-filters
69+
template: templates/core/stac-auth-proxy-filters-configmap.yaml
70+
asserts:
71+
- isKind:
72+
of: ConfigMap
73+
- equal:
74+
path: metadata.name
75+
value: eoapi-stac-auth-proxy-custom-filters
76+
- isNotEmpty:
77+
path: data

charts/eoapi/values.yaml

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -416,18 +416,45 @@ stac:
416416
stac-auth-proxy:
417417
enabled: false
418418
image:
419-
tag: "v0.11.0"
420-
env:
421-
ROOT_PATH: "/stac"
422-
OVERRIDE_HOST: "false"
423-
DEFAULT_PUBLIC: "true"
424-
# UPSTREAM_URL will be set dynamically in template to point to stac service
425-
# OIDC_DISCOVERY_URL must be configured when enabling auth
419+
tag: "v0.11.1"
426420
ingress:
427421
enabled: false # Handled by main eoapi ingress
428422
service:
429423
port: 8080
430424
resources: {}
425+
env:
426+
# OIDC_DISCOVERY_URL must be configured when enabling auth (required)
427+
ROOT_PATH: "/stac"
428+
OVERRIDE_HOST: "false"
429+
# UPSTREAM_URL will be set dynamically in template to point to stac service
430+
#
431+
# Authentication filters settings:
432+
DEFAULT_PUBLIC: "true" # This enables standard profile for authentication filters
433+
# Alternatively with the following settings custom filters can be added
434+
# These must be mounted with extraVolumes/extraVolumeMounts (see below)
435+
# COLLECTIONS_FILTER_CLS: stac_auth_proxy.custom_filters:CollectionsFilter
436+
# ITEMS_FILTER_CLS: stac_auth_proxy.custom_filters:ItemsFilter
437+
438+
# Path to custom filters file (relative to chart root)
439+
# When extraVolumes is configured, a ConfigMap will be created from this file
440+
# customFiltersFile: "data/stac-auth-proxy/custom_filters.py"
441+
442+
# Additional volumes to mount (e.g., for custom filter files)
443+
extraVolumes: []
444+
# Example:
445+
# extraVolumes:
446+
# - name: filters
447+
# configMap:
448+
# name: stac-auth-proxy-filters
449+
#
450+
# Additional volume mounts for the container
451+
extraVolumeMounts: []
452+
# Example:
453+
# extraVolumeMounts:
454+
# - name: filters
455+
# mountPath: /app/src/stac_auth_proxy/custom_filters.py
456+
# subPath: custom_filters.py
457+
# readOnly: true
431458

432459
vector:
433460
enabled: true

tests/integration/test_stac_auth.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,119 @@ def test_stac_read_operations_work(stac_endpoint: str) -> None:
106106

107107
resp = client.get(f"{stac_endpoint}/collections")
108108
assert resp.status_code == 200
109+
110+
111+
def test_stac_auth_custom_filters_mounted() -> None:
112+
"""Test that custom filters ConfigMap, env vars, and file mount work correctly."""
113+
import subprocess
114+
115+
namespace = os.getenv("NAMESPACE", "eoapi")
116+
release = os.getenv("RELEASE_NAME", "eoapi")
117+
118+
# Check ConfigMap exists
119+
result = subprocess.run(
120+
[
121+
"kubectl",
122+
"get",
123+
"configmap",
124+
"-n",
125+
namespace,
126+
"eoapi-stac-auth-proxy-custom-filters",
127+
"-o",
128+
"jsonpath={.data.custom_filters\\.py}",
129+
],
130+
capture_output=True,
131+
text=True,
132+
check=False,
133+
)
134+
if result.returncode != 0:
135+
pytest.skip("Custom filters ConfigMap not found (feature may be disabled)")
136+
137+
# Verify ConfigMap contains filter classes
138+
assert "class CollectionsFilter" in result.stdout
139+
assert "class ItemsFilter" in result.stdout
140+
141+
# Get stac-auth-proxy pod name
142+
result = subprocess.run(
143+
[
144+
"kubectl",
145+
"get",
146+
"pod",
147+
"-n",
148+
namespace,
149+
"-l",
150+
f"app.kubernetes.io/name=stac-auth-proxy,app.kubernetes.io/instance={release}",
151+
"-o",
152+
"jsonpath={.items[0].metadata.name}",
153+
],
154+
capture_output=True,
155+
text=True,
156+
check=True,
157+
)
158+
pod_name = result.stdout.strip()
159+
160+
if not pod_name:
161+
pytest.skip("stac-auth-proxy pod not found")
162+
163+
# Check env vars are set
164+
result = subprocess.run(
165+
[
166+
"kubectl",
167+
"exec",
168+
"-n",
169+
namespace,
170+
pod_name,
171+
"--",
172+
"printenv",
173+
"COLLECTIONS_FILTER_CLS",
174+
],
175+
capture_output=True,
176+
text=True,
177+
check=False,
178+
)
179+
assert result.returncode == 0, "COLLECTIONS_FILTER_CLS env var not set"
180+
assert (
181+
"stac_auth_proxy.custom_filters:CollectionsFilter" in result.stdout
182+
), f"Unexpected COLLECTIONS_FILTER_CLS value: {result.stdout}"
183+
184+
result = subprocess.run(
185+
[
186+
"kubectl",
187+
"exec",
188+
"-n",
189+
namespace,
190+
pod_name,
191+
"--",
192+
"printenv",
193+
"ITEMS_FILTER_CLS",
194+
],
195+
capture_output=True,
196+
text=True,
197+
check=False,
198+
)
199+
assert result.returncode == 0, "ITEMS_FILTER_CLS env var not set"
200+
assert (
201+
"stac_auth_proxy.custom_filters:ItemsFilter" in result.stdout
202+
), f"Unexpected ITEMS_FILTER_CLS value: {result.stdout}"
203+
204+
# Check if custom_filters.py is mounted at correct path
205+
result = subprocess.run(
206+
[
207+
"kubectl",
208+
"exec",
209+
"-n",
210+
namespace,
211+
pod_name,
212+
"--",
213+
"cat",
214+
"/app/src/stac_auth_proxy/custom_filters.py",
215+
],
216+
capture_output=True,
217+
text=True,
218+
check=True,
219+
)
220+
221+
# Verify mounted file contains expected filter classes
222+
assert "class CollectionsFilter" in result.stdout
223+
assert "class ItemsFilter" in result.stdout
224+
assert 'return "1=1"' in result.stdout

0 commit comments

Comments
 (0)