Skip to content

Split claim matching into visibility (OR) and subset (AND) - #848

Merged
rdimitrov merged 3 commits into
mainfrom
rdimitrov/validate-issue-843
Jul 17, 2026
Merged

Split claim matching into visibility (OR) and subset (AND)#848
rdimitrov merged 3 commits into
mainfrom
rdimitrov/validate-issue-843

Conversation

@rdimitrov

Copy link
Copy Markdown
Member

What

Fixes the multi-value claim bug from #843 by splitting claim matching into two rules by direction:

Rule Function Within-array Used for
Visibility (read) claimsVisible / validateClaimsVisible[Bytes] OR — share any one value per-user entry filter, registry access gate, single-resource GET, delete, source references
Subset (write) claimsContain / validateClaimsSubset AND — must hold all values create / update / publish / update-claims (incoming request claims only)

Both rules now share one key-level matcher (claimsMatch) and differ only in the within-array test.

Why

claimsContain used AND-within-array for every comparison. A resource tagged team: ["platform", "data"] was meant as an allow-list ("platform or data"), but the code required a caller to hold both values — so a user in only platform was filtered out, contradicting the documented OR-within-array rule (auth.md §3). See #843.

The naive fix (flip claimsContain to OR everywhere) would fix reads but open a privilege-escalation hole on writes: a caller could tag a resource with array values they don't fully possess, widening visibility beyond their own identity. Hence the split — visibility is OR, write-subset stays AND.

Behavior change to note

Moving deletes to the visibility rule means anyone who can see a shared-claim resource can also delete it (a caller in one of several allowed groups). This is intentional so list / get / delete agree (auth.md §4), and is documented in the reconciled §5.

Changes

  • internal/service/db/claims_filter.goclaimsVisible, validateClaimsVisible[Bytes], shared claimsMatch; claimsContain unchanged in behavior.
  • Routed 16 read/access gates to the visibility variants; 7 write sites stay on subset.
  • Docs: auth.md §3/§4/§5 rewritten to describe the two rules; data-model.md §4 reconciled.
  • Tests: new unit coverage for both rules (incl. AND-across-keys + OR-within-array); fixed the array-claims integration test, which asserted the old AND behavior under an "OR-within-array" title.

Testing

Fixes #843

🤖 Generated with Claude Code

`claimsContain` applied AND-within-array matching to every claim
comparison, so a caller holding one value of a multi-value resource
claim (e.g. `team: ["platform", "data"]`) was filtered out instead of
matching — contradicting the documented OR-within-array visibility rule
and hiding entries from users they were meant for.

Split the matcher by direction. Read/visibility checks (per-user entry
filtering, the registry access gate, single-resource reads, deletes, and
source references) now use `claimsVisible` / `validateClaimsVisible` — OR
within arrays. Write/subset checks (create, update, publish,
update-claims, against the incoming request claims) keep `claimsContain`
— AND within arrays — so a caller still cannot stamp a resource with
values they do not fully hold (privilege escalation). Both rules share
one key-level matcher (`claimsMatch`) and differ only in the within-array
test, so list and single-resource gates always agree.

Update `auth.md` §3/§5 and `data-model.md` §4 to document the two rules,
and fix the array-claims integration test, which asserted the buggy AND
behavior under an "OR-within-array" title.

Fixes #843

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.15385% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.07%. Comparing base (06a392a) to head (c70a914).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/service/db/impl_registry.go 83.33% 1 Missing ⚠️
internal/service/db/impl_source.go 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #848      +/-   ##
==========================================
+ Coverage   61.97%   62.07%   +0.10%     
==========================================
  Files         109      109              
  Lines       10698    10724      +26     
==========================================
+ Hits         6630     6657      +27     
+ Misses       3483     3482       -1     
  Partials      585      585              

☔ 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.

danbarr
danbarr previously approved these changes Jul 17, 2026
Comment thread internal/service/db/claims_filter.go Outdated
JAORMX
JAORMX previously approved these changes Jul 17, 2026
## What

Fixes #845: publishing into the managed source now requires the caller's
JWT to cover the **source's** claims — not just the entry's claims.

> ⚠️ **Stacked on #848** (base branch `rdimitrov/validate-issue-843`).
It builds on the `validateClaimsVisible*` helpers introduced there.
Review/merge #848 first; this will retarget to `main` afterward.

## Why

`POST /v1/entries` was gated by the `manageEntries` role and an
**entry-claims ⊆ JWT** check, but nothing validated the managed source's
own claims (`getManagedSource` even discarded the `Claims` field).
`auth.md` §4 already stated the intended behavior — *"tag the managed
source … otherwise no non-super-admin caller can publish to it"* — so
the code was the outlier.

**The gap:** entry claims are subset-validated, so a `contoso` writer
can't create an `{org:acme}` entry — but they *could* publish an
`{org:contoso}` entry **into acme's shared managed source**, squatting
names in the first-come-first-served namespace. This closes that
cross-tenant hole.

## Change

- `getManagedSource` now populates `Claims` (previously dropped).
- Server + skill publish transactions add a **visibility check** (caller
JWT must cover the source's claims — OR-within-array, `auth.md` §3),
mirroring how `resolveSourceIDsWithGate` gates referencing a source.
- An **untagged** managed source is now publishable only by super-admin
(default-deny, §4).
- Docs: `auth.md` §4/§5 updated.

## Scope: publish only (deliberate)

Delete and update-claims are **not** changed — they already gate on the
*entry's* own claims (`validateClaimsVisibleBytes(existing.Claims)`),
which for a published entry is the meaningful per-entry authorization.
Adding a redundant source-claims check there isn't necessary. Publish is
the actual gap (no entry exists yet to gate on).

## Semantics: visibility (OR), not subset (AND)

The check compares the caller against the source's **existing** claims,
so per the #848 taxonomy it's the **visibility** direction
(OR-within-array) — consistent with referencing a source. (The issue
tentatively assumed subset/AND; visibility keeps it uniform with every
other existing-claims gate.)

## ⚠️ Behavior change / migration note

A managed source with **no claims**, running with authz on, becomes
**super-admin-only for publishing**. This is exactly what `auth.md` §4
prescribes (unlabeled ≠ public), but it will affect existing deployments
that ran with an untagged managed source. Recovery: tag the managed
source with a tenant-wide claim (e.g. `{org: "acme"}`) in config. This
is a maintainer-facing decision — the issue explicitly flagged "fix code
vs fix spec"; this PR is the "fix code" proposal.

## Testing

- New `TestPublish{Server,Skill}Version_SourceClaimsGate` (integration):
covered / not-covered / untagged-default-deny / nil-JWT-bypass /
array-OR / super-admin-bypass
- Existing publish/delete/get/update claim tests updated (managed source
now tagged `{org:acme}`) — all green
- Full `internal/service/db` + `internal/authz` integration suites pass
(real Postgres); `go vet`, `gofmt`, `golangci-lint` clean

Fixes #845

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@rdimitrov
rdimitrov dismissed stale reviews from JAORMX and danbarr via dd8a96d July 17, 2026 12:43
@rdimitrov
rdimitrov requested review from JAORMX and danbarr July 17, 2026 12:49
@rdimitrov
rdimitrov enabled auto-merge (squash) July 17, 2026 12:53
danbarr
danbarr previously approved these changes Jul 17, 2026
Co-authored-by: Dan Barr <danbarr@users.noreply.github.com>
@rdimitrov
rdimitrov disabled auto-merge July 17, 2026 13:09
@rdimitrov
rdimitrov merged commit edeb9ed into main Jul 17, 2026
13 checks passed
@rdimitrov
rdimitrov deleted the rdimitrov/validate-issue-843 branch July 17, 2026 13:09
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.

claimsContain requires ALL values of a multi-value claim, contradicting documented OR-within-array semantics

5 participants