Skip to content

feat: configurable path param regex - #208

Open
ceholden wants to merge 2 commits into
mainfrom
feat/config-path-param-regex
Open

feat: configurable path param regex#208
ceholden wants to merge 2 commits into
mainfrom
feat/config-path-param-regex

Conversation

@ceholden

Copy link
Copy Markdown

Description

Closes #206

This PR extends the collections_path_filter and items_path_filter to handle both, (1) the scoping/coverage of auth requirements and (2) the data extraction from those paths.

#204 is related to this change since it updated the data extraction to support anyone who added "queryables" from the Filter extension to the collections_path_filter, but it isn't necessarily replaced by this PR. I think it's good to cover the Filter API extension out of the box since it's so common. This PR is more intended to support any endpoint hosted alongside STAC API routes.

How it works

The user can provide named capture groups inside the regular expressions patterns collections_path_filter and items_path_filter to control how the data extraction happens. This gives the user an ability to override the hard coded behavior of the utils.requests.extract_variables() function.

Including named capture groups implies supporting a plural number of filter patterns. We can match on many endpoint path styles using just one regex, but we cannot have the same named capture group appear multiple times in the same regular expression. Accordingly the type signatures and some of the handling had to be expanded to support more than one pattern. For example, find_match moves to an any(re.match(pattern, path) for pattern in patterns) instead of checking a single re.match(pattern, path).

I took as an assumption that this should be backwards compatible. Anyone who overrides collections_path_filter/items_path_filter without using named capture groups will default to use the utils.requests.extract_variables() function as a fallback. If this fallback didn't exist then regex without named capture groups would provide no data, breaking their evaluations. I can remove this backup if it'd be better to make this a v2 major version breaking change.

The use case I have isn't for the STAC API aggregation extension, but I used that as an example in the updated configuration documentation since it's similar enough and a published STAC API extension.


🤖 disclosure: I used a LLM to assist investigating a solution, to write unit tests, and to provide review feedback, but I've reviewed and modified any generated code.

@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown

Claude finished @ceholden's task in 3m 48s —— View job


Reviewing PR #208

  • Gather context (diff, files, CLAUDE.md)
  • Review correctness / security / best-practice issues
  • Ponytail pass for over-engineering
  • Post final review

✅ No blocking issues — safe to merge.

Traced the full path-param flow (config.pyCql2BuildFilterMiddleware._get_filter/_path_paramsutils/requests.find_match/as_patterns/extract_variables) and the fallback-to-builtin-extraction logic. It's consistent across all four call sites (Cql2BuildFilterMiddleware, AuthenticationExtensionMiddleware, OpenApiMiddleware, find_match), and the new tests (test_cql2_build_filter_middleware.py::TestFilterPathParams, test_config.py::test_items_and_collections_path_parameters) cover the interesting cases: multi-pattern lists, non-participating regex alternatives not leaking None, and the no-named-groups fallback.

Non-blocking observations:

  • src/stac_auth_proxy/config.py:50str2patterns reuses the x.startswith("[") heuristic from str2list to decide "JSON array vs single string." For comma-lists that's safe, but a single regex pattern that legitimately starts with a character class (e.g. r"[0-9]{4}/collections$") would be routed into json.loads instead of treated as one pattern. JSONDecodeError is a ValueError subclass so Pydantic still surfaces a clean validation error rather than crashing — just a confusing message ("Expecting value...") instead of "not a valid regular expression." Every default/example pattern in this PR starts with ^, so this is unlikely to bite in practice; worth a doc note if it does.

Simplify (ponytail)

  • config.py:37 str2patterns vs utils/requests.py:29 as_patterns — both normalize str | Sequence[str] | Nonelist[str]; str2patterns's non-string branch could just delegate to as_patterns instead of re-implementing list(x).
  • Cql2BuildFilterMiddleware.py:35-41 — one-element-tuple defaults ((r"...",)) already flagged by @vincentsarago as unusual; a plain list default reads the same without the "is this a typo" pause.
  • Cql2BuildFilterMiddleware.py:47-53re.compile(pattern) is called again here just to log the no-named-groups fallback notice; re.compile is internally cached so it's not a real cost, but the check could reuse the already-compiled pattern from _get_filter's re.match if this ever needs tightening.

💰 Estimated review cost: $0.75 · 3m48s · 30 turns

@github-actions github-actions Bot added the feat label Aug 31, 2026
@ceholden
ceholden force-pushed the feat/config-path-param-regex branch from 43d66bf to fb5f2ef Compare August 31, 2026 19:25
Comment thread src/stac_auth_proxy/middleware/AuthenticationExtensionMiddleware.py Outdated
collections_filter_path: str = r"^/collections(/[^/]+)?$"
collections_filter_path: Union[str, Sequence[str]] = (
r"^/collections(?:/(?P<collection_id>[^/]+))?$",
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can the usage of sequence be avoided by using | in regex?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would break if we have the same named capture group in each arm of the regex,

>>> parts = [
...     r"^/collections/(?P<collection_id>[^/]+)/aggregate$",
...     r"^/collections/(?P<collection_id>[^/]+)/aggregations$",
... ]
# Compiles fine
>>> for p in parts:
...     re.compile(p)
...
re.compile('^/collections/(?P<collection_id>[^/]+)/aggregate$')
re.compile('^/collections/(?P<collection_id>[^/]+)/aggregations$')
# Boom!
>>> re.compile("|".join(parts))
...
re.PatternError: redefinition of group name 'collection_id' as group 2; was group 1 at position 68

def __post_init__(self):
"""Set required conformances based on the filter functions."""
for attr in ("collections_filter_path", "items_filter_path"):
object.__setattr__(self, attr, requests.as_patterns(getattr(self, attr)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not something we usually see in python 😬

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙈 I can unroll the loop to avoid getattr/setattr since it's n=2

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgotten from Friday... object.__setattr__ is needed since the class is frozen. Worth a comment at least!

I could try removing frozen=True on the class, keep it as it is, or switch this to be a Pydantic dataclass with a "before" validator

"method": request.method,
"query_params": dict(request.query_params),
"path_params": requests.extract_variables(request.url.path),
"path_params": path_params,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The more I think about it the more I think the parsing for path_params should be done in the filter function, so we don't need customization here

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I following along here? The way I'm reading this would imply:

  • path_params as it exists now would only be relevant for core (+ filter) STAC API endpoints
  • Anything beyond that would have to rerun the regex that handles matching on the endpoint in order to extract the pattern
  • The consequence is the reuse of the covering regex with the extraction regex in this PR wouldn't be needed, so we could close as not needed
  • This stance would consider the queryables / filter extension support as a special case that we're not going to extend further

We do get the request.url.path so it's totally possible if we want to limit the scope here! Or, am I misunderstanding what you meant?

@codecov

codecov Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.07843% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.90%. Comparing base (8b3d3c9) to head (6a67f9c).

Files with missing lines Patch % Lines
src/stac_auth_proxy/config.py 90.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #208      +/-   ##
==========================================
+ Coverage   89.65%   89.90%   +0.25%     
==========================================
  Files          30       30              
  Lines        1343     1377      +34     
  Branches      182      191       +9     
==========================================
+ Hits         1204     1238      +34     
  Misses         97       97              
  Partials       42       42              
Flag Coverage Δ
unittests 89.90% <96.07%> (+0.25%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ceholden
ceholden marked this pull request as ready for review August 31, 2026 19:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generalize path parameter extraction beyond the core STAC layout

2 participants