Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
10d6aa9
feat(keyverse): add OIDC authorizer package
seonghobae Aug 15, 2026
717bf78
docs(keyverse): define offline verifier boundary
seonghobae Aug 15, 2026
6a93eee
feat(keyverse): define verifier configuration and ports
seonghobae Aug 15, 2026
2997179
feat(keyverse): implement strict offline JWT authorizer
seonghobae Aug 15, 2026
e8229fa
feat(keyverse): publish OIDC authorizer API
seonghobae Aug 15, 2026
6cf3fea
chore(keyverse): mark authorizer package as typed
seonghobae Aug 15, 2026
dd95d54
test(keyverse): add deterministic JOSE fixtures
seonghobae Aug 15, 2026
4875720
test(keyverse): cover strict verifier configuration
seonghobae Aug 15, 2026
52a806d
test(keyverse): verify valid access token workflow
seonghobae Aug 15, 2026
ba3f82b
test(keyverse): cover JOSE header and key-set failures
seonghobae Aug 15, 2026
0a710a0
test(keyverse): cover registered and private claim failures
seonghobae Aug 15, 2026
e06734d
test(keyverse): translate provider and identity mapping failures
seonghobae Aug 15, 2026
ee28b30
test(keyverse): cover strict JOSE helper boundaries
seonghobae Aug 15, 2026
a395abc
test(keyverse): cover configuration helper boundaries
seonghobae Aug 15, 2026
6a654c5
test(keyverse): enforce public authorizer docstrings
seonghobae Aug 15, 2026
d4cc402
test(keyverse): enforce offline and asymmetric verifier contract
seonghobae Aug 15, 2026
8713483
build(keyverse): pin authorization test dependencies
seonghobae Aug 15, 2026
0a2abbd
ci(keyverse): add strict authorization quality gate
seonghobae Aug 15, 2026
b0099f3
test(keyverse): enforce immutable and secret-minimal CI
seonghobae Aug 15, 2026
c5822ea
docs(keyverse): record offline JWT authorizer decision
seonghobae Aug 15, 2026
fe3c762
docs(keyverse): publish strict access-token profile
seonghobae Aug 15, 2026
668dd70
docs(keyverse): define OIDC authorizer threat model
seonghobae Aug 15, 2026
d803e93
docs(keyverse): add APA 7 JOSE and access-token references
seonghobae Aug 15, 2026
26cd479
docs(keyverse): trace strict authorization requirements to evidence
seonghobae Aug 15, 2026
38af059
docs(keyverse): add offline authorization sequences
seonghobae Aug 15, 2026
5a6cb91
docs(keyverse): record unreleased authorization slice
seonghobae Aug 15, 2026
3e73072
build(keyverse): pin package verification tools
seonghobae Aug 15, 2026
c3ed6b8
ci(keyverse): add package build and installed-wheel smoke
seonghobae Aug 15, 2026
fc98b75
test(keyverse): enforce package workflow integrity
seonghobae Aug 15, 2026
23baa6a
fix(auth): use Python 3.14-compatible Pydantic lock
seonghobae Aug 16, 2026
a8323f4
fix(auth): enforce JWK verification key operations
seonghobae Aug 16, 2026
8ae912a
docs(auth): preserve authorizer API docstring gate
seonghobae Aug 16, 2026
602f634
merge: refresh Keyverse authorization dependency stack
seonghobae Aug 16, 2026
52a3b94
merge(stack): synchronize current People API dependency
seonghobae Aug 16, 2026
7fc36ef
test(keyverse): require exact-head authorization checkout
seonghobae Aug 16, 2026
5d33db8
test(keyverse): require exact-head package checkout
seonghobae Aug 16, 2026
45edcd7
fix(ci): bind Keyverse quality to exact pull request head
seonghobae Aug 16, 2026
dc8a008
fix(ci): bind Keyverse package to exact pull request head
seonghobae Aug 16, 2026
4ac5e18
test: require independent OAuth operation scopes
seonghobae Aug 16, 2026
c7968f1
test: issue scoped access-token fixtures
seonghobae Aug 16, 2026
be3cb50
test: exercise independent scope and purpose grants
seonghobae Aug 16, 2026
02756ce
fix: enforce OAuth operation scope independently
seonghobae Aug 16, 2026
81dba84
test: update hostile claim cases for scope-bound authorizer
seonghobae Aug 16, 2026
f840558
test: bind JOSE failures to required operation scope
seonghobae Aug 16, 2026
89f5881
test: preserve dependency failures under scope-bound authorization
seonghobae Aug 16, 2026
73eb43d
test: cover scope parser security branches
seonghobae Aug 16, 2026
6abb46f
docs: correct OAuth scope standards traceability
seonghobae Aug 16, 2026
2bb08f7
docs: specify independent RFC 9068 operation scopes
seonghobae Aug 16, 2026
63eeb39
docs: bind Keyverse authorization to scope and purpose
seonghobae Aug 16, 2026
adfe53e
docs: show independent scope and purpose authorization
seonghobae Aug 16, 2026
44a1232
docs: trace independent OAuth scope enforcement
seonghobae Aug 16, 2026
a004b86
chore: record independent Keyverse scope enforcement
seonghobae Aug 16, 2026
a7fa891
fix: reject stripped controls and own identity claim validation
seonghobae Aug 16, 2026
1ac9230
fix: reject controls before config normalization
seonghobae Aug 16, 2026
82790df
test: keep hostile purpose cases JSON-representable
seonghobae Aug 16, 2026
9bd7301
test: keep JOSE integration cases encoder-realistic
seonghobae Aug 16, 2026
4110eee
fix: reject hidden controls across verifier configuration
seonghobae Aug 16, 2026
73eb43b
test: cover hidden controls in verifier config
seonghobae Aug 16, 2026
a9502fa
refactor: simplify compact token validation
seonghobae Aug 16, 2026
2ee3bbf
test: require installed local dependency chain in wheel smoke
seonghobae Aug 16, 2026
c299244
fix: install declared local dependencies in package smoke
seonghobae Aug 16, 2026
78a06c6
test: leave null registered-claim handling to JWT profile
seonghobae Aug 16, 2026
94f5715
test: cover strict JOSE and authorization character branches
seonghobae Aug 16, 2026
d67af62
fix: keep People API dependencies server-owned
cursoragent Aug 16, 2026
00c339c
docs: record People API runtime dependency boundary
cursoragent Aug 16, 2026
16590b0
merge: refresh People API dependency stack
cursoragent Aug 16, 2026
af6d0b2
chore: bind integrity manifest to the People API stack
cursoragent Aug 16, 2026
f4595f1
fix: align People API CI lock with Python 3.14 Pydantic
cursoragent Aug 16, 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/keyverse-auth-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: Keyverse Authorization Package

on:
pull_request:
paths:
- "integrations/keyverse-auth/**"
- "services/people-api/**"
- "packages/orgmetra-postgres/**"
- "requirements/keyverse-auth-ci.txt"
- "requirements/keyverse-auth-package-ci.txt"
- ".github/workflows/keyverse-auth-package.yml"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: keyverse-auth-package-${{ github.event.pull_request.number || github.ref }}-${{ github.sha }}
cancel-in-progress: false

jobs:
package:
name: Python ${{ matrix.python_version }} package
runs-on: ubuntu-24.04
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
python_version:
- "3.12"
- "3.14"
env:
PIP_DISABLE_PIP_VERSION_CHECK: "1"
PIP_NO_INPUT: "1"
steps:
- name: Check out exact pull request head
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: ${{ matrix.python_version }}
cache: pip
cache-dependency-path: |
requirements/keyverse-auth-ci.txt
requirements/keyverse-auth-package-ci.txt
- name: Install exact package verification dependencies
run: >-
python -m pip install
--only-binary=:all:
--no-deps
-r requirements/keyverse-auth-ci.txt
-r requirements/keyverse-auth-package-ci.txt
- name: Build declared local dependency wheels
run: |
python -m build --wheel --no-isolation packages/orgmetra-postgres
python -m build --wheel --no-isolation services/people-api
- name: Build wheel and source distribution without network isolation
run: python -m build --no-isolation integrations/keyverse-auth
- name: Inspect package contents
run: |
python - <<'PY'
from pathlib import Path
from zipfile import ZipFile

wheels = list(Path("integrations/keyverse-auth/dist").glob("*.whl"))
if len(wheels) != 1:
raise SystemExit(f"expected one wheel, found {len(wheels)}")
with ZipFile(wheels[0]) as archive:
names = set(archive.namelist())
required = {
"orgmetra_keyverse_auth/__init__.py",
"orgmetra_keyverse_auth/authorizer.py",
"orgmetra_keyverse_auth/contracts.py",
"orgmetra_keyverse_auth/py.typed",
}
missing = required - names
if missing:
raise SystemExit(f"wheel is missing required files: {sorted(missing)}")
sdists = list(Path("integrations/keyverse-auth/dist").glob("*.tar.gz"))
if len(sdists) != 1:
raise SystemExit(f"expected one source distribution, found {len(sdists)}")
PY
- name: Install local dependency chain and smoke test outside the source tree
run: |
python -m pip install --no-deps --force-reinstall \
packages/orgmetra-postgres/dist/*.whl \
services/people-api/dist/*.whl \
integrations/keyverse-auth/dist/*.whl
cd "$(mktemp -d)"
env -u PYTHONPATH python - <<'PY'
from orgmetra_keyverse_auth import KeyverseOidcAuthorizer, KeyverseOidcConfig

assert KeyverseOidcAuthorizer
assert KeyverseOidcConfig
print("installed wheel smoke: ok")
PY
- name: Verify dependency consistency
run: python -m pip check
92 changes: 92 additions & 0 deletions .github/workflows/keyverse-auth-quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Keyverse Authorization Quality

on:
pull_request:
paths:
- "integrations/keyverse-auth/**"
- "services/people-api/**"
- "packages/orgmetra-postgres/**"
- "requirements/keyverse-auth-ci.txt"
- ".github/workflows/keyverse-auth-quality.yml"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: keyverse-auth-quality-${{ github.event.pull_request.number || github.ref }}-${{ github.sha }}
cancel-in-progress: false

jobs:
keyverse-auth-quality:
name: Python ${{ matrix.python_version }}
runs-on: ubuntu-24.04
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
python_version:
- "3.12"
- "3.14"
env:
PYTHONPATH: integrations/keyverse-auth/src:services/people-api/src:packages/orgmetra-postgres/src
PIP_DISABLE_PIP_VERSION_CHECK: "1"
PIP_NO_INPUT: "1"
steps:
- name: Check out exact pull request head
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: ${{ matrix.python_version }}
cache: pip
cache-dependency-path: requirements/keyverse-auth-ci.txt
- name: Install exact pinned dependencies
run: >-
python -m pip install
--only-binary=:all:
--no-deps
-r requirements/keyverse-auth-ci.txt
- name: Record exact security dependency versions
run: |
python - <<'PY'
from importlib.metadata import version

for package_name in ("PyJWT", "cryptography"):
print(f"{package_name}={version(package_name)}")
PY
- name: Compile shipped packages
run: |
python -m compileall -q integrations/keyverse-auth/src
python -m compileall -q services/people-api/src
python -m compileall -q packages/orgmetra-postgres/src
- name: Run authorization tests with exact coverage
run: >-
python -m coverage run
--branch
--source=orgmetra_keyverse_auth
-m pytest
integrations/keyverse-auth/tests
- name: Enforce production statement and branch coverage
run: |
python -m coverage report --show-missing --fail-under=100
python -m coverage json -o keyverse-auth-coverage.json
python - <<'PY'
import json

with open("keyverse-auth-coverage.json", encoding="utf-8") as report_file:
totals = json.load(report_file)["totals"]
if totals["percent_covered"] != 100.0:
raise SystemExit("production statement coverage is not 100%")
if totals["num_branches"] and totals["covered_branches"] != totals["num_branches"]:
raise SystemExit("production branch coverage is not 100%")
PY
- name: Enforce public docstring coverage
run: python integrations/keyverse-auth/tests/validate_docstrings.py
- name: Verify import and dependency consistency
run: |
python -c "import orgmetra_keyverse_auth; print(orgmetra_keyverse_auth.__all__)"
python -m pip check
4 changes: 4 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,7 @@ Access is tenant, actor, purpose, resource, and lifetime scoped. High-impact dec
## Active implementation slice: bitemporal domain kernel

`packages/orgmetra-domain` implements framework-independent invariants for effective/system time, distinct HRIS records, multiple assignments, and candidate-worker continuity. It has no persistence or transport dependency and can be embedded by future services. This section describes active-PR work until merged into the protected default branch.

## Active implementation slice: purpose-bound People API

`services/people-api` is an independently importable FastAPI factory. Hosts inject `TokenAuthorizer` and `PeopleRepository`. Protected routes select both an OAuth operation scope and a finer HR purpose in server code. `PurposeContext` and the repository port are runtime `Depends` values, not caller query fields. This section describes active-PR work until merged into the protected default branch.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,20 @@ All notable changes to Orgmetra will be documented in this file.

- Organization and job mutable descriptions now live in version records rather than durable identity anchors, preserving normalized history and stable position references.
- Effective and system-recorded intervals are non-empty half-open periods; equal start/end bounds are rejected.
- People API route handlers now inject `PurposeContext` and the repository through runtime `Depends` defaults so callers cannot supply those objects as query fields.

### Fixed

- People API Quality no longer fails with HTTP 422 on every protected route when FastAPI 0.116 interprets postponed annotations as query parameters.
- Bearer token parsing now splits only on the first ASCII space so C0 separators such as `\\x1f` remain visible and are rejected.
- People API CI now pins the same Python 3.14-compatible Pydantic lock as Keyverse authorization and includes `certifi` so `pip check` can close the httpx dependency set.

### Security

- Purpose-bound PII access contract.
- LLM output constrained to draft evidence.
- No direct cross-service database access.
- Reject malformed, duplicate, or non-verification JWK `key_ops` declarations before constructing a signing key.

### Notes

Expand Down
27 changes: 27 additions & 0 deletions changes/0007-keyverse-oidc-authorizer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Unreleased: strict Keyverse JWT authorizer

## Added

- independently importable offline Keyverse JWT access-token authorizer
- strict HTTPS issuer, audience, access-token type and asymmetric algorithm profile
- injected JWK Set and identity-reference resolver ports
- exact signing-key selection and registered/private claim validation
- RFC 9068/RFC 8693 space-delimited operation-scope validation independent from HR business-purpose validation
- bounded positive token lifetime, duplicate-free scope grants and duplicate-free purpose grants
- retryable identity-provider failure classification
- Python 3.12/3.14 exact coverage and public docstring gate

## Security

- no verifier-owned discovery, DNS, HTTP, cache, persistence, environment or logging authority
- no HMAC, unsigned token, embedded caller key, ambiguous duplicate-key or incompatible JWK `key_ops` path
- a valid business purpose cannot enlarge a token that lacks the route's OAuth operation scope
- external subject and tenant identifiers are mapped to opaque Orgmetra references
- token, claims, JWK material and identity strings are not copied into audit records
- CI uses minimal permissions, immutable action revisions and no model credentials

## Known limits

Discovery/JWKS acquisition, SSRF-safe egress, signed key cache and rotation,
revocation/account-disable propagation, identity-link lifecycle, live Keyverse
conformance, SBOM/provenance and external security review remain release gates.
68 changes: 68 additions & 0 deletions docs/UML_KEYVERSE_AUTH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Keyverse authorization UML views

## Component boundary

```mermaid
flowchart LR
token["Untrusted bearer token"] -->|"Compact JWT"| verifier["KeyverseOidcAuthorizer"]
route["Server-selected route policy"] -->|"Operation scope + HR purpose"| verifier
verifier -->|"Exact issuer"| keys["Injected JwksProvider"]
verifier -->|"Issuer + subject + tenant"| mapping["Injected IdentityReferenceResolver"]
keys -.->|"Future SSRF-safe adapter"| keyverse["Keyverse JWKS"]
mapping -.->|"Future identity-link adapter"| identity["Orgmetra identity links"]
verifier -->|"Opaque references + scopes + purposes"| principal["AuthorizedPrincipal"]
principal -->|"Defensive scope + purpose checks"| api["Orgmetra People API"]
```

## Successful verification sequence

```mermaid
sequenceDiagram
participant API as People API
participant Auth as KeyverseOidcAuthorizer
participant Keys as JwksProvider
participant Resolver as IdentityReferenceResolver

API->>Auth: authorize(bearer token, route scope, route purpose)
Auth->>Auth: Validate route scope and purpose vocabulary
Auth->>Auth: Validate compact JWT, alg, typ and kid
Auth->>Keys: get_jwks(exact issuer)
Keys-->>Auth: Bounded JWK Set
Auth->>Auth: Select exactly one compatible verification key
Auth->>Auth: Verify signature, issuer, audience and time
Auth->>Auth: Validate tenant, subject, jti, standard scope and purposes
Auth->>Auth: Require route scope AND route purpose independently
Auth->>Resolver: resolve(issuer, subject, tenant external id)
Resolver-->>Auth: Opaque tenant and actor references
Auth-->>API: AuthorizedPrincipal(scopes, purposes)
```

## Failure classification sequence

```mermaid
sequenceDiagram
participant API as People API
participant Auth as KeyverseOidcAuthorizer
participant Keys as JwksProvider
participant Resolver as IdentityReferenceResolver

API->>Auth: authorize(token, scope, purpose)
alt Malformed token or bad signature/claims
Auth-->>API: AuthenticationFailed
else Operation scope absent
Auth-->>API: AuthorizationDenied
else Business purpose absent
Auth-->>API: AuthorizationDenied
else Keys unavailable or ambiguous
Auth->>Keys: get_jwks(issuer)
Keys-->>Auth: Outage or invalid key set
Auth-->>API: IdentityProviderUnavailable
else Identity mapping unavailable
Auth->>Resolver: resolve external identities
Resolver-->>Auth: Outage or invalid result
Auth-->>API: IdentityProviderUnavailable
end
```

These views describe active-PR architecture only. They become protected-main
truth after dependency merges and fresh integrated review and checks.
7 changes: 6 additions & 1 deletion docs/adr/0006-purpose-bound-people-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ these contracts:
problem documents with a random `err_...` support reference and actionable
`next_action`;
10. internal trace references never appear in response headers or problem bodies;
11. Swagger and ReDoc UI remain disabled in the pre-GA service.
11. Swagger and ReDoc UI remain disabled in the pre-GA service;
12. People API route modules keep runtime annotations and `Depends` defaults so
FastAPI cannot treat `PurposeContext`, `Request`, or the repository port as
caller query fields. `from __future__ import annotations` is forbidden in
`app.py` because postponed annotations made those server-owned objects look
like request input under FastAPI 0.116.

## Alternatives considered

Expand Down
Loading
Loading