Skip to content

feat(access_control): Implement scope validation for per-resource authorization - #13708

Open
eveniota wants to merge 4 commits into
apache:masterfrom
eveniota:access-control/scope-claim-validation
Open

eveniota wants to merge 4 commits into
apache:masterfrom
eveniota:access-control/scope-claim-validation

Conversation

@eveniota

Copy link
Copy Markdown

Fixes #13607

Summary

The access_control plugin previously parsed and exposed a scope claim and supported the --invalid-scope-status-code configuration option, but AccessToken::validate() never evaluated the scope against incoming request paths. Consequently, any valid token authorized access to every resource covered by a remap rule, and setting --invalid-scope-status-code produced no effect.
This PR finishes the feature by implementing path-prefix scope validation on normalized path segments, providing per-resource granularity while remaining fully backward compatible with existing tokens.

Design Approach (per #13607 suggestion)

This PR implements the minimal backward-compatible approach:

  1. Path-prefix matching on normalized segments
  2. Absent/empty scope = unrestricted (backward compatible)
  3. Scope check in separate function after token validation

These choices are open to feedback; see Design Decisions section below.

Key Requirements & Design Decisions

  1. Clean Separation of Concerns (Plumbing):

    • Kept AccessToken::validate() focused purely on token cryptographic integrity, semantics, and timing.
    • Implemented scope comparison as a separate helper function, validateScope(requestPath, scope), called in enforceAccessControl() immediately after token->validate() succeeds.
    • When out of scope, the transaction state is set to OUT_OF_SCOPE, correctly triggering --invalid-scope-status-code (default: 403) and suppressing subject header extraction.
  2. Matching Semantics (Normalized Path Segments):

    • Matching is performed as a path-prefix on normalized segments.
    • Handles trailing slashes, redundant slashes, and ATS's TSUrlPathGet() format (which omits leading slashes).
    • Enforces directory segment boundaries so that a token scoped to /reports/ (or /reports) authorizes /reports/2026/ but strictly rejects sibling paths like /reports2/ or /reports_backup.
  3. Backward Compatibility (Absent / Empty Scope):

    • An absent or empty scope claim is treated as unrestricted. Existing tokens in the field continue to function without disruption.
  4. Resource Granularity vs Target Audience (sub claim):

    • Retains the existing model where sub represents target audience (e.g. frogs-in-a-well), while adding the missing per-resource granularity (e.g. scope="/reports/2026/").

What Changed

  • Core Matching Logic (access_control.cc, access_control.h):

    • Declared and implemented validateScope(StringView requestPath, StringView scope).
    • Added helper normalizePath() to ensure leading slashes, collapse consecutive slashes, and normalize trailing slashes.
    • Updated @todo comments regarding scope validation.
  • Transaction Enforcement (plugin.cc):

    • Inside enforceAccessControl(), extracted the request path via TSUrlPathGet() and validated it against token->getScope().
    • On failure, sets data->_vaState = OUT_OF_SCOPE and invokes handleInvalidToken().
  • Unit Tests (test_access_control.cc):

    • Added Catch2 test cases covering:
      • Empty scope (unrestricted / backward compatibility)
      • Exact match (/reports $\rightarrow$ /reports)
      • Subpath matches (/reports $\rightarrow$ /reports/2026/annual.pdf)
      • Sibling segment boundary enforcement (/reports vs /reports2 $\rightarrow$ rejected)
      • Inverted hierarchy / parent path requests with child scope $\rightarrow$ rejected
      • Normalization edge cases (trailing slashes, redundant slashes, root scope "/")
      • Token builder & parser integration with addScope() / getScope()
    • Documentation (access_control.en.rst):
      • Replaced the note stating scope is "ignored by the current version of the plugin" with full documentation of the matching semantics, segment boundary rules, and status code behavior.

Future Work

  • Pattern matching (e.g., /reports/{year}/)
  • Scope lists or disjunctions
  • Host-qualified scopes (e.g., example.com:/reports/)
  • Query parameter scoping

These can be added in follow-ups once the core prefix matching is proven.

Verification

  • Catch2 unit tests pass: ./build/plugins/experimental/access_control/unit_tests/test_access_control
  • Code formatted via cmake --build build -t format

Copilot AI lite review requested due to automatic review settings September 19, 2026 09:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Documentation and API wording review comments remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 Low severity

Open (2)
What changed in this PR

Implements per-resource path-scope validation for the experimental access_control plugin.

Changes:

  • Adds normalized path-prefix matching with backward-compatible empty scopes.
  • Enforces scope checks during authorization.
  • Adds unit tests and updates documentation.
File Description
plugins/​experimental/​access_control/​unit_tests/​test_access_control.cc Scope matching and integration tests
plugins/​experimental/​access_control/​plugin.cc Scope enforcement integration
plugins/​experimental/​access_control/​access_control.h Scope validation API
plugins/​experimental/​access_control/​access_control.cc Path normalization and validation
doc/​admin-guide/​plugins/​access_control.en.rst Scope behavior documentation

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

* ``tid`` for `token id`_, `optional`
* ``ver`` for `version`_, `optional`, defaults to ``ver=1`` if not specified.
* ``scope`` for `scope`_, `optional`, ignored by the current version of the plugin, still not finalized (more applications and their use cases need to be studied to finalize the format)
* ``scope`` for `scope`_, `optional`, A path-prefix scope that restricts token use to matching request paths. Matching is performed on normalized path segments;
Comment on lines +111 to +113
* Validates whether a request path fails within the scope claim of an access token.
* Matching is performed on normalized path segments. An empty or absent scope is
* treated as unrestricted (returns true).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

access_control: implement the scope claim so a token authorizes only its own resources

2 participants