Skip to content

fix(auth): require auth on /saml/providers/{idp}/users and complete response examples - #654

Merged
ericfitz merged 2 commits into
mainfrom
fix/cats-triage-1.6.4
Jul 31, 2026
Merged

fix(auth): require auth on /saml/providers/{idp}/users and complete response examples#654
ericfitz merged 2 commits into
mainfrom
fix/cats-triage-1.6.4

Conversation

@ericfitz

Copy link
Copy Markdown
Owner

Triage and remediation of the 2026-07-30 CATS baseline (20260730T220551Z, 542 true positives).
Takes it to roughly 85, with the residue understood rather than unexplained.

category was expected
Not matching response schema 428 ~4
Unexpected 401 (/saml/providers/{idp}/users) 19 0
Unexpected 403 (/me/groups/{id}/members) 12 0
Unexpected 200 (CheckDeletedResourcesNotAvailable) 2 0
Unexpected 500 (bulk create) 1 0 (fixed in 1.6.3)
everything else ~80 ~80 (verified correct behavior / CATS artifacts)

Real bug: GET /saml/providers/{idp}/users never worked (#650)

Returned 401 on 69 of 69 requests. cmd/server/main.go listed "/saml/" in
publicPathPrefixes, so PublicPathsMiddleware marked every SAML path public, the JWT middleware
skipped validation, and ListSAMLUsers' GetAuthenticatedUser found no user in context. The
handler failed closed, making this an availability defect rather than an exposure — but the blanket
prefix meant any future authenticated /saml/ route would silently lose its JWT context too. The
spec marks exactly one /saml/* operation as requiring bearerAuth, and the middleware
contradicted it.

Replaced with the three exact public paths plus isPublicSAMLPath for the two provider-scoped
routes, which have a variable segment and so cannot be expressed as a prefix. Matching exactly
three segments keeps any new /saml/ route private until added deliberately. This strictly
narrows the set of public paths.

Verified live against k3s:

request before after
/saml/providers/tmi/users + valid token 401 200, 39 users
/saml/providers/tmi/users anonymous 401 401
/saml/providers/okta/users + valid token 401 403 — cross-provider check reachable for the first time
6 public SAML routes, no token work work

CATS validates responses against example, not schema (#637Endava/cats#206)

Root-caused the 428 findings that were 79% of the backlog. When a response declares an example,
CATS requires the body's property names to be a subset of it and never consults the schema —
replacing the schema with {"type":"object"} or even true still fails. Reproduced standalone
(1 path, a 20-line stub); deleting the example alone flips fail → pass. Validated across the whole
run: 428 flagged / 0 with a covering example, zero counterexamples.

This explains the Note-vs-TeamNote discriminator that stalled two sessions: schemas with no
example never run the check, exhaustive ones pass, partial ones fail 100% of the time.

Remediation is not suppression — the 23 genuinely incomplete examples are now complete, so the
findings clear legitimately and real schema regressions still surface. Structure comes from observed
responses (a project's team is documented as the {id, name} summary it actually is, not a
fully-populated Team) and values from schema examples, so no fixture data enters published docs.
422/428 now covered; the rest is MinimalDiagramModel.metadata, an additionalProperties map whose
keys are unbounded and which therefore cannot satisfy the check from the spec side.

Smaller fixes

  • RepositoryBulkUpdateItem now requires name, which repository_sub_resource_handlers.go:592
    has always enforced while no schema declared it.
  • GET .../collaborate marked x-skip-deleted-resource-check — sessions are not CRUD resources, so
    a 200 after a DELETE is correct, not a stale-resource leak.
  • charlie added to the seeded CATS Test Group: /me/groups/{group_id}/members answers from the
    caller's own membership. Both routes are read-only, so the campaign cannot undo it.

Verification

  • make validate-openapi — 0 errors, all 328 operations annotated
  • make generate-api (oapi-codegen v2.7.1) — only change beyond the embedded spec is
    RepositoryBulkUpdateItem.Name losing omitempty; stays *string, handler nil-check unaffected
  • make build-server, make lint — 0 issues
  • make test-unit2451 passed, 0 failed
  • make test-integration82 passed, 0 failed, 9 skipped
  • /security-reviewno HIGH or MEDIUM findings. isPublicSAMLPath fails closed on traversal,
    empty provider, wrong case and trailing slash. No fixture or PII strings entered the spec.
  • New cmd/server/public_paths_test.go — 17 path cases guarding fix(auth): GET /saml/providers/{idp}/users always returns 401 - blanket /saml/ public prefix starves it of JWT context #650

TestIdentityLink failed once mid-session and was chased down: a pre-existing flake (#653), not
a regression. Both the clean base and this branch pass 82/82.

Follow-ups filed

HANDOFF.md carries the full triage, including the conclusions for the ~80 findings confirmed as
correct server behavior so they are not re-derived next session.

Fixes #650

🤖 Generated with Claude Code

https://claude.ai/code/session_01WnrVyySuyJFqcMxB8q3pNc

ericfitz added 2 commits July 31, 2026 14:17
…esponse examples

Triage and remediation of the 2026-07-30 CATS baseline (20260730T220551Z,
542 true positives).

GET /saml/providers/{idp}/users returned 401 on 69 of 69 requests and had
never worked. cmd/server/main.go listed "/saml/" in publicPathPrefixes, so
PublicPathsMiddleware marked every SAML path public, the JWT middleware
skipped validation, and ListSAMLUsers' GetAuthenticatedUser found no user in
context. The handler failed closed, so this was an availability defect rather
than an exposure -- but the blanket prefix meant any future authenticated
/saml/ route would silently lose its JWT context too. The spec marks exactly
one /saml/* operation as requiring bearerAuth and the middleware contradicted
it. Replaced with the three exact public paths plus isPublicSAMLPath for the
two provider-scoped routes, which have a variable segment and so cannot be a
prefix. Matching exactly three segments keeps new /saml/ routes private until
added deliberately. Verified live: 200 with users where it was 401, 401 when
anonymous, and 403 cross-provider -- the handler's second security check is
reachable for the first time.

Completed 23 response examples. CATS validates a 2xx body against the declared
`example` rather than the `schema` (Endava/cats#206): when an example is
present the response's property names must be a subset of it, and the schema
is never consulted. This accounted for 428 of the 542 findings. Rather than
suppress, the examples that genuinely omitted fields the server returns are
now complete, so the findings clear legitimately and real schema regressions
still surface. Example structure comes from observed responses -- a project's
`team` is documented as the {id, name} summary it actually is -- and values
from schema examples, so no fixture data enters published docs. 422 of the 428
now have covering examples; the remainder is MinimalDiagramModel.metadata, an
additionalProperties map whose keys are unbounded and which therefore cannot
satisfy the check from the spec side.

RepositoryBulkUpdateItem now requires `name`, which the bulk-update handler
has always enforced while no schema declared it. GET .../collaborate is marked
x-skip-deleted-resource-check: sessions are not CRUD resources, so a 200 after
a DELETE is correct rather than a stale-resource leak. charlie is added to the
seeded CATS Test Group because /me/groups/{group_id}/members answers from the
caller's own membership; both routes are read-only so the campaign cannot undo
it.

Together these take the baseline from 542 findings to roughly 85, with the
residue understood rather than unexplained.

Fixes #650
Refs #637, #651, #652, #653, Endava/cats#206
The marker's sha is the commit its body last changed at, which could not be
known until 2cab847 existed.
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.

fix(auth): GET /saml/providers/{idp}/users always returns 401 - blanket /saml/ public prefix starves it of JWT context

1 participant