Skip to content
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
140 commits
Select commit Hold shift + click to select a range
ce4663c
feat: name Weekly VOC and focus Event Lineage (v2.13.0)
seonghobae Aug 19, 2026
03a1167
feat: open Calendar commitments onto Event Lineage (v2.14.0)
seonghobae Aug 19, 2026
d25eeda
feat: open Customer master related posts onto Event Lineage (v2.15.0)
seonghobae Aug 19, 2026
0aaf992
feat: open Ask Agent cited posts onto Event Lineage (v2.16.0)
seonghobae Aug 19, 2026
3e5bfca
feat: keep GNB Event Lineage focus on a linked DAG node (v2.17.0)
seonghobae Aug 19, 2026
8966294
ci: repair analysis-run DAG cutoff context
seonghobae Aug 19, 2026
cad7327
feat: focus Keyman after GNB Event Lineage names it next (v2.19.0)
seonghobae Aug 19, 2026
7c28bc1
feat: expose authenticated MCP Global Ask
seonghobae Aug 19, 2026
50c67f3
chore: trigger verified MCP lock refresh
seonghobae Aug 19, 2026
9254d6f
ci: verify MCP lock on pull request
seonghobae Aug 19, 2026
5df719b
fix: fail closed on uncited Global Ask answers
seonghobae Aug 19, 2026
e03bd42
fix: use the MCP v2 Context generic contract
seonghobae Aug 19, 2026
61d8bb2
test: reject uncited MCP Global Ask answers
seonghobae Aug 19, 2026
8bcf538
fix: fail closed on malformed issuer JWKS
seonghobae Aug 19, 2026
e428e46
test: cover malformed issuer JWKS key sets
seonghobae Aug 19, 2026
7211a5e
fix: mark Global Ask as closed-world evidence access
seonghobae Aug 19, 2026
62f7802
test: cover closed-world metadata and Host validation
seonghobae Aug 19, 2026
c6b75ce
ci: run every MCP regression during lock refresh
seonghobae Aug 19, 2026
69b8ee5
fix: use supported conduct mode for evidence chat
seonghobae Aug 19, 2026
a2e6e8f
test: lock supported conduct chat contract
seonghobae Aug 19, 2026
0b7ee33
docs: align MCP operation with conduct verification
seonghobae Aug 19, 2026
0b4bfc5
feat(mcp): add external Global Ask claim verification
seonghobae Aug 19, 2026
6537dd7
ci: verify lock and supported conduct contract once
seonghobae Aug 19, 2026
6615ac3
test(mcp): cover external Global Ask verification
seonghobae Aug 19, 2026
d2dac29
feat(mcp): expose separate external corroboration
seonghobae Aug 19, 2026
2edaf6e
docs: record conduct and citation trust fixes
seonghobae Aug 19, 2026
656d118
test(mcp): expose external verification separately
seonghobae Aug 19, 2026
fd0b146
docs: record supported conduct orchestration decision
seonghobae Aug 19, 2026
6926311
fix(mcp): make external verification explicit and privacy-bounded
seonghobae Aug 19, 2026
5468e5b
fix(mcp): require explicit opt-in for open-web verification
seonghobae Aug 19, 2026
a9cc99b
test(mcp): require explicit web-verification opt-in
seonghobae Aug 19, 2026
3685b5a
fix(mcp): require cited web evidence for verification verdicts
seonghobae Aug 19, 2026
adf591d
test(mcp): lock external verification evidence contract
seonghobae Aug 19, 2026
138d402
docs(mcp): ground external claim verification
seonghobae Aug 19, 2026
da8b857
docs(mcp): remove chat-only citation marker
seonghobae Aug 19, 2026
517949b
docs(mcp): record opt-in external claim verification
seonghobae Aug 19, 2026
0267dee
test(mcp): prove web verification is opt-in
seonghobae Aug 19, 2026
0dd0d95
test(mcp): execute opt-in boundary asynchronously
seonghobae Aug 19, 2026
0bd3012
fix(mcp): accept only whole structured verification responses
seonghobae Aug 19, 2026
d32f241
fix(mcp): harden explicit external verification boundary
seonghobae Aug 19, 2026
e42e74d
ci: implement multimodal MCP Global Ask
seonghobae Aug 20, 2026
8ed5015
feat: name Weekly VOC and focus Event Lineage (v2.13.0)
seonghobae Aug 19, 2026
3990fc9
feat: open Calendar commitments onto Event Lineage (v2.14.0)
seonghobae Aug 19, 2026
f9b86fc
docs: assign Calendar focus ADR a unique number
seonghobae Aug 20, 2026
609b61d
fix: stop auto-drilled Keyman panel from stealing Event Lineage focus
seonghobae Aug 20, 2026
a5831c0
feat: open Customer master related posts onto Event Lineage (v2.15.0)
seonghobae Aug 19, 2026
73887c9
fix: preserve Event Lineage focus after related loads
seonghobae Aug 20, 2026
cbdcc6f
feat: open Ask Agent cited posts onto Event Lineage (v2.16.0)
seonghobae Aug 19, 2026
1eeac6b
feat: keep GNB Event Lineage focus on a linked DAG node (v2.17.0)
seonghobae Aug 19, 2026
b5caf7c
fix(ui): preserve analysis-run cutoff across DAG navigation
seonghobae Aug 20, 2026
36ef783
feat: open Customer master related posts onto Event Lineage (v2.15.0)
seonghobae Aug 19, 2026
29c9732
test: keep customer master router specific
seonghobae Aug 20, 2026
1c37126
test(red): reproduce stale post detail navigation
seonghobae Aug 20, 2026
b39fd35
feat: open Ask Agent cited posts onto Event Lineage (v2.16.0)
seonghobae Aug 19, 2026
48461d0
merge: restack onto feat/ask-agent-open-focus-event-lineage-v2160
seonghobae Aug 20, 2026
bd08c68
merge: restack onto feat/event-lineage-node-keeps-gnb-focus-v2170
seonghobae Aug 20, 2026
c910478
feat: name Weekly VOC and focus Event Lineage (v2.13.0)
seonghobae Aug 19, 2026
85db4e1
fix: reject impossible Weekly VOC dates
seonghobae Aug 20, 2026
8daa419
feat: open Calendar commitments onto Event Lineage (v2.14.0)
seonghobae Aug 19, 2026
156a507
fix: stop auto-drilled Keyman panel from stealing Event Lineage focus
seonghobae Aug 20, 2026
5267614
fix: keep Calendar scroll with Event Lineage
seonghobae Aug 20, 2026
dc00316
feat: open Customer master related posts onto Event Lineage (v2.15.0)
seonghobae Aug 19, 2026
15c82c7
test: keep customer master router specific
seonghobae Aug 20, 2026
8fb0fea
feat: open Ask Agent cited posts onto Event Lineage (v2.16.0)
seonghobae Aug 19, 2026
7f36683
feat: keep GNB Event Lineage focus on a linked DAG node (v2.17.0)
seonghobae Aug 19, 2026
a731551
fix(ui): preserve analysis-run cutoff across DAG navigation
seonghobae Aug 20, 2026
4d6494d
fix: ignore stale post detail responses
seonghobae Aug 20, 2026
a7f3c12
merge: restack onto latest feat/event-lineage-node-keeps-gnb-focus-v2170
seonghobae Aug 20, 2026
3cf8305
test: install the async pytest runtime
seonghobae Aug 20, 2026
a6937b4
fix(mcp): enforce transport security before OAuth
seonghobae Aug 20, 2026
a2f1b57
test(mcp): bind pre-auth transport security
seonghobae Aug 20, 2026
35ccc77
docs: record the pre-auth MCP transport boundary
seonghobae Aug 20, 2026
812be55
build: lock MCP runtime and conduct contract
github-actions[bot] Aug 20, 2026
fd3ed26
fix(ci): remove stale MCP architecture rewrite
seonghobae Aug 20, 2026
178f355
fix(mcp): delegate post context to orchestrator auto mode
seonghobae Aug 20, 2026
362a6fe
build: lock MCP runtime
github-actions[bot] Aug 20, 2026
2df52bd
fix(config): prefer home gateway credentials
seonghobae Aug 20, 2026
d789c30
merge: restack onto latest feat/event-lineage-node-keeps-gnb-focus-v2170
seonghobae Aug 20, 2026
8bbc2ed
chore: restack Keyman focus on current Event Lineage head
seonghobae Aug 20, 2026
5e3e241
chore: reconcile remote Keyman restack with verified tree
seonghobae Aug 20, 2026
251efe1
chore: restack Keyman focus on current Event Lineage head
seonghobae Aug 20, 2026
4506f26
fix: align static SQL contract with safe composition
seonghobae Aug 20, 2026
b333958
chore: restack Keyman focus on current Event Lineage head
seonghobae Aug 20, 2026
7d881db
chore: restack Keyman focus on final Event Lineage
seonghobae Aug 20, 2026
4ff6fa2
chore: restack Keyman focus on final Event Lineage
seonghobae Aug 20, 2026
387427e
chore: restack Keyman focus on final Event Lineage
seonghobae Aug 20, 2026
afe8c2d
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 20, 2026
0229c4b
test: align embedded image accessibility expectation
seonghobae Aug 20, 2026
dc15a23
Merge commit '85013814fd65114c5a75d04297f04e342129b8d1' of https://gi…
seonghobae Aug 20, 2026
86c5f88
fix: avoid keyman ADR number collision
seonghobae Aug 20, 2026
306718f
docs: give authenticated MCP ADR a unique number
seonghobae Aug 20, 2026
55eb838
docs: remove duplicate authenticated MCP ADR number
seonghobae Aug 20, 2026
884fed2
Merge commit '55eb8385de2877ce7ba38b0ba4fbfa798aa21c51' of https://gi…
seonghobae Aug 20, 2026
733c697
fix: isolate gateway tests and reject weak relation hits
seonghobae Aug 20, 2026
95f19d0
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 20, 2026
eb4594d
docs: assign unique ADR number to Valkey events
seonghobae Aug 20, 2026
74c2234
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 20, 2026
e44f1a6
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 20, 2026
a005e7e
Merge current event-lineage base into GNB lineage
seonghobae Aug 20, 2026
284879a
feat: expose lineage timeline and cited images in MCP Ask
seonghobae Aug 20, 2026
60b44c4
merge: propagate current protected Event Lineage base
seonghobae Aug 20, 2026
75cdf43
merge: propagate current Event Lineage branch head
seonghobae Aug 20, 2026
a3fdaa9
merge: propagate current protected main
seonghobae Aug 20, 2026
26a6d9c
merge: propagate current semantic-source Event Lineage base
seonghobae Aug 20, 2026
10aced6
fix: harden authenticated MCP evidence paths
seonghobae Aug 20, 2026
0e311a6
test(mcp): require live media authorization
seonghobae Aug 20, 2026
3e73cae
fix(mcp): reauthorize cited media at read time
seonghobae Aug 20, 2026
210cd25
fix(mcp): pass live account identity to media query
seonghobae Aug 20, 2026
3a24c6b
test(keycloak): require live MCP audience reconciliation
seonghobae Aug 20, 2026
0fbbd42
feat(keycloak): reconcile persistent MCP audience mapper
seonghobae Aug 20, 2026
2c6fb8f
feat(keycloak): gate MCP on audience reconciliation
seonghobae Aug 20, 2026
f08dadd
docs(adr): preserve live Global Ask authorization
seonghobae Aug 20, 2026
bba02ff
docs(mcp): document live media and audience reconciliation
seonghobae Aug 20, 2026
9a2f949
docs(changelog): record live MCP authorization fixes
seonghobae Aug 20, 2026
432d9e9
docs(doctoring): trace Keycloak and live media boundaries
seonghobae Aug 20, 2026
7f7676f
test: align MCP media index with raster filtering
seonghobae Aug 20, 2026
661704b
Merge remote-tracking branch 'origin/agent/authenticated-mcp-global-a…
seonghobae Aug 20, 2026
c58aef8
test(chat): require SQL-level source visibility
seonghobae Aug 20, 2026
98bd90b
ci: repair MCP source visibility boundary
seonghobae Aug 20, 2026
fa68e99
ci: expose source-visibility repair to PR checks
seonghobae Aug 20, 2026
4508a1d
fix(auth): filter chat sources before body load
seonghobae Aug 20, 2026
0d9e863
test(auth): prove bodies load only after ABAC
seonghobae Aug 20, 2026
e8e383a
ci: remove completed source-visibility repair workflow
seonghobae Aug 20, 2026
4a76186
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 20, 2026
b34050d
merge: restack authenticated MCP on buyer navigation
seonghobae Aug 20, 2026
9f87e64
merge: sync authenticated ask branch with main
seonghobae Aug 20, 2026
eb83c9c
merge: preserve concurrent buyer navigation restack
seonghobae Aug 20, 2026
1881a3c
chore: normalize MCP import ordering
seonghobae Aug 20, 2026
bbbe734
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 21, 2026
e14cb9f
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 21, 2026
5a67017
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 21, 2026
e1c31db
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 21, 2026
259b3b0
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 21, 2026
0beb110
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 21, 2026
515bb0b
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 21, 2026
8a6a1b6
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 21, 2026
c8b7445
fix: remove self-modifying repair artifacts
seonghobae Aug 21, 2026
4d74f77
Merge remote-tracking branch 'origin/feat/gnb-event-lineage-focus-key…
seonghobae Aug 21, 2026
79687c9
fix: serialize MCP-boundary provider calls
seonghobae Aug 21, 2026
c0fcdb6
fix: keep malformed timeline dates sortable
seonghobae Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions .github/workflows/mcp-lock-refresh.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: Refresh MCP dependency lock once

on:
push:
paths:
- pyproject.toml
- .github/workflows/mcp-lock-refresh.yml
pull_request:
types: [opened, synchronize, reopened]
paths:
- pyproject.toml
- uv.lock
- backend/app/mcp_*.py
- backend/app/global_ask.py
- backend/app/global_ask_verification.py
- lineageweave/post_chat.py
- tests/test_mcp_*.py
- tests/test_global_ask_external_verification.py
- tests/test_post_chat_*.py
- .github/workflows/mcp-lock-refresh.yml

concurrency:
group: mcp-lock-refresh-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: write
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

jobs:
refresh-lock:
if: >-
github.actor != 'github-actions[bot]' &&
github.repository_owner == 'ContextualWisdomLab' &&
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
runs-on: ubuntu-latest
steps:
- name: Checkout feature branch
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7
with:
ref: ${{ github.head_ref || github.ref_name }}
fetch-depth: 0
persist-credentials: true

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6
with:
python-version: "3.12"

- name: Set up locked dependency manager
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version: "0.11.28"
enable-cache: false

- name: Resolve only the declared dependency change
run: uv lock

- name: Remove the superseded verify-mode architecture caveat
run: |
python - <<'PY'
from pathlib import Path

path = Path("ARCHITECTURE.md")
text = path.read_text(encoding="utf-8")
start_marker = "> **Known local-test-environment limitation:**"
end_marker = "unaffected.\n"
assert text.count(start_marker) == 1, "expected one stale verify-mode architecture block"
start = text.index(start_marker)
end = text.index(end_marker, start) + len(end_marker)
replacement = (
"> **Contextual-orchestrator contract:** Post Ask and MCP Global Ask use the\n"
"> currently supported `mode=\"conduct\"` path for verified multi-step\n"
"> reasoning. Model-based verification requires fast-mlsirm in the same\n"
"> contextual-orchestrator runtime and fails closed when that dependency or\n"
"> its contract is unavailable. LineageWeave never falls back to a direct\n"
"> provider or the rejected legacy `verify` mode.\n"
)
path.write_text(text[:start] + replacement + text[end:], encoding="utf-8")
PY

- name: Verify the frozen backend environment and MCP regressions
run: |
uv sync --frozen --extra dev --extra backend
uv run --frozen python -m pytest -q \
tests/test_mcp_*.py \
tests/test_global_ask_external_verification.py \
tests/test_post_chat_*.py

- name: Commit the regenerated lock and architecture contract
env:
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add uv.lock ARCHITECTURE.md
if git diff --cached --quiet; then
echo "lock and architecture already match the product contract"
exit 0
fi
git commit -m "build: lock MCP runtime and conduct contract"
git push origin "HEAD:${TARGET_BRANCH}"
42 changes: 42 additions & 0 deletions CHANGELOG.d/2.13.0-authenticated-mcp-global-ask.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 2.13.0 — Authenticated MCP Global Ask

## Added

- Dedicated Streamable HTTP MCP resource server for Codex and other MCP clients.
- Read-only, idempotent `global_ask` tool over authorized source-post and
Event-Lineage evidence, with bounded retrieval and citation identities.
- Explicit `verify_external=true` open-web verification lane: Searxng retrieves
bounded public evidence and contextual-orchestrator classifies the internal
answer as `supported`, `refuted`, or `insufficient_evidence` using only those
retrieved passages.
- External verification returns separately cited public evidence URLs; those
URLs never become LineageWeave posts or internal source authority.
- Codex bearer-token configuration and production OAuth deployment guidance.

## Changed

- Post Ask and MCP Global Ask use contextual-orchestrator's supported
`mode="conduct"` workflow instead of the rejected legacy `mode="verify"`
request.
- `global_ask` advertises `open_world_hint=true` because callers can explicitly
opt into Searxng web verification; the default remains `verify_external=false`.
- The evidence-chat timeout is 300 seconds so high-reasoning orchestration is
finite but not cut off by the previous 60-second default.

## Security

- Exact MCP audience, issuer, expiry, and mandatory JWKS `kid` validation with
one bounded JWKS refresh for issuer key rotation.
- Malformed issuer JWKS key collections fail closed as service-unavailable
instead of escaping as an untyped error.
- Existing database-backed `post_read`, affiliation, and public-or-corporate
ABAC checks apply to every retrieved internal source.
- MCP DNS-rebinding Host/Origin validation is enabled; inbound tokens are never
forwarded to contextual-orchestrator.
- An internal answer with no citation inside the authorized source bundle is
rejected instead of returning unsupported prose.
- Open-web verification never runs without explicit caller opt-in, and the
private internal answer body is never used as the Searxng search query.
- External snippets are treated as untrusted data. `supported` and `refuted`
require at least one valid cited HTTP(S) evidence URL; otherwise the verdict
is downgraded to `insufficient_evidence`.
4 changes: 2 additions & 2 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@ RUN uv sync --frozen --no-dev --extra backend --no-editable \
&& chown -R appuser:appuser /app

USER appuser
EXPOSE 8000
CMD ["uvicorn", "backend.app.main:app", "--host", "0.0.0.0", "--port", "8000"]
EXPOSE 8000 8001
CMD ["uvicorn", "backend.app.main:app", "--host", "0.0.0.0", "--port", "8000"]
132 changes: 96 additions & 36 deletions backend/app/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import json
from dataclasses import dataclass
from typing import Any

import asyncpg
import jwt
Expand All @@ -25,32 +26,99 @@
from lineageweave.http_client import HttpClientError, get_json

_bearer_scheme = HTTPBearer(auto_error=True)
_jwks_cache: dict[str, dict] = {}
_jwks_cache: dict[str, dict[str, Any]] = {}


def _jwks(settings: Settings) -> dict:
class _SigningKeyNotFound(ValueError):
"""No unique RSA signing key matches a structurally valid token header."""


def _jwks(settings: Settings) -> dict[str, Any]:
"""Return the realm JWKS, cached per URI for the process lifetime."""
cached = _jwks_cache.get(settings.keycloak_jwks_uri)
if cached is None:
try:
cached = get_json(settings.keycloak_jwks_uri, timeout=10)
payload = get_json(settings.keycloak_jwks_uri, timeout=10)
except (HttpClientError, OSError, ValueError) as exc:
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE,
f"could not fetch JWKS from {settings.keycloak_jwks_uri}: {exc}",
) from exc
if not isinstance(payload, dict):
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, "issuer JWKS is not an object")
cached = payload
_jwks_cache[settings.keycloak_jwks_uri] = cached
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
return cached


def _signing_key_from_jwks(jwks: dict, token: str):
"""Pick the JWKS RSA key that matches the JWT kid, without urllib."""
header = jwt.get_unverified_header(token)
def _signing_key_from_jwks(jwks: dict[str, Any], token: str):
"""Return the sole RSA signing key matching the token's mandatory ``kid``."""
try:
header = jwt.get_unverified_header(token)
except jwt.PyJWTError as exc:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, f"invalid token header: {exc}") from exc
kid = header.get("kid")
for key in jwks.get("keys", []):
if kid is None or key.get("kid") == kid:
return RSAAlgorithm.from_jwk(json.dumps(key))
raise HTTPException(status.HTTP_401_UNAUTHORIZED, f"no JWKS key matched kid={kid!r}")
if not isinstance(kid, str) or not kid:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "invalid token: missing kid")
keys = jwks.get("keys")
if not isinstance(keys, list):
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE,
"issuer JWKS keys is not an array",
)
matches = [
key
for key in keys
if isinstance(key, dict)
and key.get("kid") == kid
and key.get("kty") == "RSA"
and key.get("use") in {None, "sig"}
]
if len(matches) != 1:
raise _SigningKeyNotFound(f"expected one RSA signing key for kid={kid!r}")
try:
return RSAAlgorithm.from_jwk(json.dumps(matches[0]))
except (KeyError, TypeError, ValueError, jwt.PyJWTError) as exc:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "invalid token signing key") from exc


def decode_access_token(
token: str,
settings: Settings,
*,
audience: str | None = None,
) -> dict[str, Any]:
"""Validate signature, issuer, time claims, and an optional resource audience."""
try:
try:
signing_key = _signing_key_from_jwks(_jwks(settings), token)
except _SigningKeyNotFound:
# A new issuer key may have appeared after this process cached JWKS.
# Refresh exactly once; persistent ambiguity/miss is still a 401.
_jwks_cache.pop(settings.keycloak_jwks_uri, None)
try:
signing_key = _signing_key_from_jwks(_jwks(settings), token)
except _SigningKeyNotFound as exc:
raise HTTPException(
status.HTTP_401_UNAUTHORIZED, f"invalid token: {exc}"
) from exc
return jwt.decode(
token,
key=signing_key,
algorithms=["RS256"],
issuer=settings.keycloak_issuer,
audience=audience,
options={"verify_aud": audience is not None},
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
except HTTPException:
raise
except jwt.PyJWTError as exc:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, f"invalid token: {exc}") from exc


def _decode_access_token(token: str, settings: Settings) -> dict[str, Any]:
"""Backward-compatible REST decoder; MCP uses an exact audience instead."""
return decode_access_token(token, settings)


@dataclass(frozen=True)
Expand All @@ -68,29 +136,10 @@ def has_permission(self, permission_code: str) -> bool:
return permission_code in self.permission_codes


def _decode_access_token(token: str, settings: Settings) -> dict:
try:
signing_key = _signing_key_from_jwks(_jwks(settings), token)
return jwt.decode(
token,
key=signing_key,
algorithms=["RS256"],
issuer=settings.keycloak_issuer,
options={"verify_aud": False},
)
except jwt.PyJWTError as exc:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, f"invalid token: {exc}") from exc


async def get_current_account(
credentials: HTTPAuthorizationCredentials = Depends(_bearer_scheme),
pool: asyncpg.Pool = Depends(get_pool),
) -> CurrentAccount:
"""Resolve the bearer token to a provisioned ``user_account`` row."""
settings = load_settings()
claims = _decode_access_token(credentials.credentials, settings)
subject = claims["sub"]

async def resolve_current_account(pool: asyncpg.Pool, subject: str) -> CurrentAccount:
"""Resolve one verified subject to DB-owned affiliations and permissions."""
if not subject:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "token has no subject")
async with pool.acquire() as conn:
account_row = await conn.fetchrow(
"select user_account_id, display_name from user_account where external_subject_id = $1",
Expand All @@ -102,7 +151,6 @@ async def get_current_account(
"token is valid but no user_account is provisioned for this subject "
"(run scripts/seed_demo_data.py, or provision the account, first)",
)

entity_rows = await conn.fetch(
"select corporate_entity_id from account_affiliation where user_account_id = $1",
account_row["user_account_id"],
Expand All @@ -116,11 +164,23 @@ async def get_current_account(
""",
account_row["user_account_id"],
)

return CurrentAccount(
user_account_id=str(account_row["user_account_id"]),
external_subject_id=subject,
display_name=account_row["display_name"],
corporate_entity_ids=frozenset(str(row["corporate_entity_id"]) for row in entity_rows),
permission_codes=frozenset(row["permission_code"] for row in permission_rows),
permission_codes=frozenset(str(row["permission_code"]) for row in permission_rows),
)


async def get_current_account(
credentials: HTTPAuthorizationCredentials = Depends(_bearer_scheme),
pool: asyncpg.Pool = Depends(get_pool),
) -> CurrentAccount:
"""Resolve the REST bearer token to a provisioned ``user_account`` row."""
settings = load_settings()
claims = _decode_access_token(credentials.credentials, settings)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
subject = claims.get("sub")
if not isinstance(subject, str):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "token has no subject")
return await resolve_current_account(pool, subject)
Loading
Loading