Split claim matching into visibility (OR) and subset (AND) - #848
Merged
Conversation
`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 Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
This was referenced Jul 17, 2026
danbarr
previously approved these changes
Jul 17, 2026
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>
danbarr
previously approved these changes
Jul 17, 2026
Co-authored-by: Dan Barr <danbarr@users.noreply.github.com>
stacklokbot
approved these changes
Jul 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes the multi-value claim bug from #843 by splitting claim matching into two rules by direction:
claimsVisible/validateClaimsVisible[Bytes]claimsContain/validateClaimsSubsetBoth rules now share one key-level matcher (
claimsMatch) and differ only in the within-array test.Why
claimsContainused AND-within-array for every comparison. A resource taggedteam: ["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 onlyplatformwas filtered out, contradicting the documented OR-within-array rule (auth.md§3). See #843.The naive fix (flip
claimsContainto 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.go—claimsVisible,validateClaimsVisible[Bytes], sharedclaimsMatch;claimsContainunchanged in behavior.auth.md§3/§4/§5 rewritten to describe the two rules;data-model.md§4 reconciled.Testing
internal/service/db+internal/authz✅, including the end-to-end array-claims test (aplatform-only user now correctly sees ateam:[platform,data]entry)go vet(incl.-tags integration),gofmt,golangci-lint(0 issues) ✅Fixes #843
🤖 Generated with Claude Code