From 453c6a24ec2a9accabc52edfa453f83d9f9ca87d Mon Sep 17 00:00:00 2001 From: Jake Shilling Date: Fri, 24 Apr 2026 12:26:27 -0400 Subject: [PATCH] docs: resolve documentation audit findings for 2026-04-24 --- AGENTS.md | 2 +- README.md | 31 +-- api-spec/README.md | 16 +- docs/api.md | 140 ++++++-------- docs/architecture.md | 29 ++- docs/audit/2026-04-24_12-25-05.md | 179 ++++++++++++++++++ docs/features/README.md | 2 +- docs/features/core/nsc-education.md | 13 +- docs/features/infrastructure/redis.md | 6 +- docs/features/security/cognito-auth.md | 69 ++++--- docs/guides/01-getting-started.md | 12 +- docs/guides/02-authentication.md | 5 + docs/guides/03-usage-examples.md | 4 +- docs/overview.md | 130 +++++++------ .../public-api-data-model-design-first.md | 16 +- docs/setup.md | 5 +- 16 files changed, 415 insertions(+), 244 deletions(-) create mode 100644 docs/audit/2026-04-24_12-25-05.md diff --git a/AGENTS.md b/AGENTS.md index e329193..4019d29 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,7 +41,7 @@ Observed entry points: - `main.go` - `api.New` - `routes.RegisterRoutes` -- `api-spec/openapi.yaml` +- `api-spec/v0/openapi.yaml` Observed deployment helpers: diff --git a/README.md b/README.md index 9b7d7ee..efe580a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The Emmy API is a backend data service that connects to federal and commercial d - **[About Emmy Software](https://cms.gov/eligibility-made-easy)** - [Emmy API Overview](https://cms.gov/eligibility-made-easy) - - [Emmy Application Github](https://github.com/DSACMS/iv-cbv-payroll/blob/main/README.md) + - [Emmy Application GitHub](https://github.com/DSACMS/iv-cbv-payroll/blob/main/README.md) - **[Technical Guide to Getting Started](docs/guides/01-getting-started.md)** - [Usage Examples](docs/guides/03-usage-examples.md) @@ -33,30 +33,12 @@ This project uses [pre-commit](https://pre-commit.com/ "pre-commit Docs") to reg pre-commit install ``` -For OpenAPI spec maintenance, the repo also includes utility scripts under -`scripts/`: +For OpenAPI spec maintenance, the supported local workflow is exposed through +`mise` tasks: ```sh -# 1. Rebuild the bundled YAML and JSON artifacts from the design-time source spec -./scripts/bundle-api-spec - -# 2. Validate the bundled YAML artifact -./scripts/validate-api-spec - -# 3. Lint the bundled YAML artifact with the repo Spectral ruleset -./scripts/lint-api-spec - -# 4. Lint hand-authored YAML files -./scripts/lint-yaml-files - -# 5. Check formatting for contract YAML and JSON files -./scripts/check-format-contract-files - -# 6. Compile standalone JSON Schemas -./scripts/check-json-schemas - -# 7. Check for breaking OpenAPI changes against a base ref -./scripts/check-openapi-breaking [base-ref] +# Run the full local contract/spec workflow +mise run check-contract-files ``` If you use [mise](https://mise.jdx.dev/), install the pinned runtimes from @@ -74,6 +56,9 @@ You can also run each step individually with `mise run bundle-api-spec`, `mise run validate-api-spec`, `mise run lint-api-spec`, `mise run lint-yaml-files`, `mise run check-format-contract-files`, `mise run check-json-schemas`, and `mise run check-openapi-breaking`. +The direct helper scripts currently present in `scripts/` are +`./scripts/lint-yaml-files`, `./scripts/check-format-contract-files`, +`./scripts/check-json-schemas`, and `./scripts/check-openapi-breaking`. ## Policies diff --git a/api-spec/README.md b/api-spec/README.md index 7f99be5..79de208 100644 --- a/api-spec/README.md +++ b/api-spec/README.md @@ -42,19 +42,21 @@ The public contract in this branch is currently defined in ## Bundling And Checks -Use the repo scripts from the project root: +Use the supported local tasks from the project root: ```sh -./scripts/bundle-api-spec -./scripts/validate-api-spec -./scripts/lint-api-spec +mise install +mise run bundle-api-spec +mise run validate-api-spec +mise run lint-api-spec ``` -Or, with `mise`: +Equivalent `pnpm` commands: ```sh -mise install -mise run check-api-spec +pnpm run bundle:api-spec +pnpm run validate:api-spec +pnpm run lint:api-spec ``` Bundling produces checked-in versioned artifacts under `api-spec/v0/dist/` and diff --git a/docs/api.md b/docs/api.md index bab46a2..666d135 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2,81 +2,53 @@ ## Overview -The server port is configured through `PORT` and defaults to `3000` in code; -the local example environment sets `PORT=8000`. The intended public API -contract for this branch is defined in `api-spec/v0/openapi.yaml`; this page -documents the currently wired Go runtime endpoints and their operational -caveats. - -## Authentication Behavior - -- When `SKIP_AUTH=false`, Cognito middleware is enabled globally. -- Middleware reads access token from header: `x-amzn-oidc-accesstoken`. -- Token checks include: - - valid signature via JWKS - - issuer match - - `token_use=access` - - `client_id` claim equals configured app client ID - -If auth fails, response is `401 Unauthorized`. -This applies to `/api/edu` and `/api/v0/veteran-disability-ratings` when auth is -enabled. `/health` is registered before the auth middleware and remains -unauthenticated in the current branch. +The server port is configured through `PORT` and defaults to `3000` in code. +The local example environment sets `PORT=8000`. The intended public contract +for this branch is defined in `api-spec/v0/openapi.yaml`; this page documents +the currently wired Go runtime behavior. -## Circuit Breaker Behavior +## Request Identity Behavior -`/health`, `/api/edu`, and `/api/v0/veteran-disability-ratings` are wrapped by -Redis-backed circuit breaker middleware. +`api.New` always installs `SubjectMiddleware`, which populates `c.Locals("sub")` +using this precedence: -- On breaker deny/open state: `503 Service Unavailable`. -- On Redis state read failures with fail-open (default): request is allowed. +1. `X-Sub` +2. `Authorization: Bearer ` parsed without signature verification, using + only the token `sub` claim +3. fallback value `unknown-subject` -## Runtime Endpoints +When `SKIP_AUTH=true`, `SkipAuthMiddleware` is also installed and injects a +deterministic local identity before `SubjectMiddleware` runs. Optional override +headers are: -| Method | Path | Description | Success | Notes | -| ------ | ------------------------------ | -------------------------------------- | ----------- | ------------------------------------------------------------------ | -| `GET` | `/` | Liveness string | `200` text | Returns `Backend running!` | -| `GET` | `/health` | Redis health check | `200` empty | Registered before auth middleware; pings Redis with 2s timeout | -| `GET` | `/api/edu` | NSC education verification scaffold | `200` JSON | Uses a hardcoded request payload in handler; not the v0 contract | -| `POST` | `/api/v0/veteran-disability-ratings` | Veteran disability status from v0 spec | `200` JSON | Accepts caller-provided identity payload and matches the v0 route | - -| Method | Path | Description | Success | Notes | -| ------ | --------------------- | ----------------------------------- | ----------- | ----- | -| `GET` | `/` | Liveness string | `200` text | Returns `Backend running!` | -| `GET` | `/status` | Redis health check | `200` empty | Uses 2s Redis ping timeout; wrapped by circuit breaker | -| `GET` | `/api-spec/v1/verify` | Bundled OpenAPI JSON artifact | `200` JSON | Returns `api-spec/dist/openapi.bundled.json` | -| `GET` | `/api/edu` | Education verification passthrough | `200` JSON | Uses hardcoded request payload in handler; wrapped by circuit breaker | - -### NSC Submit Request model (`pkg/education/models_request.go`) - -```go -type Request struct { - AccountID string `json:"accountId"` - OrganizationName string `json:"organizationName,omitempty"` - CaseReferenceID string `json:"caseReferenceId,omitempty"` - ContactEmail string `json:"contactEmail,omitempty"` - DateOfBirth string `json:"dateOfBirth"` - LastName string `json:"lastName"` - FirstName string `json:"firstName"` - SSN string `json:"ssn,omitempty"` - IdentityDetails []IdentityDetails `json:"identityDetails,omitempty"` - EndClient string `json:"endClient"` - PreviousNames []PreviousName `json:"previousNames,omitempty"` - Terms string `json:"terms"` -} -``` +- `x-skip-auth-sub` +- `x-skip-auth-username` +- `x-skip-auth-scope` +- `x-skip-auth-groups` -### NSC Submit Response model (`pkg/education/models_response.go`) +The current branch does not wire an active Cognito verifier into the Fiber app. +Treat the checked-in v0 auth scheme as contract documentation rather than a +runtime-enforced guarantee. -```go -type Response struct { - ClientData ClientDataResponse `json:"clientData"` - IdentityDetails []IdentityDetailsResponse `json:"identityDetails"` - Status StatusResponse `json:"status"` - StudentInfoProvided StudentInfoProvidedResponse `json:"studentInfoProvided"` - TransactionDetails TransactionDetailsResponse `json:"transactionDetails"` -} -``` +## Circuit Breaker Behavior + +`GET /health`, `POST /api/v0/education-enrollments`, and +`POST /api/v0/veteran-disability-ratings` are wrapped by the Redis-backed +circuit-breaker middleware. + +- Open breaker or breaker error response: `503 Service Unavailable` +- Redis read failures follow the breaker package's `FailOpen=true` default and + allow the request through + +## Runtime Endpoints + +| Method | Path | Description | Success | Notes | +|---|---|---|---|---| +| `GET` | `/` | Liveness string | `200` text | Returns `Backend running!` | +| `GET` | `/health` | Redis health check | `200` empty | Uses a 2-second Redis ping timeout and is circuit-breaker wrapped | +| `GET` | `/api-spec/v1/verify` | Bundled OpenAPI JSON artifact | `200` JSON | Serves `api-spec/v0/dist/openapi.bundled.json` | +| `POST` | `/api/v0/education-enrollments` | NSC education verification | `200` JSON | Parses caller JSON and requires `firstName`, `lastName`, and `dateOfBirth` | +| `POST` | `/api/v0/veteran-disability-ratings` | Veteran disability lookup | `200` JSON | Requires `firstName`, `lastName`, `dateOfBirth`, and either `ssn` or a full address | ## Example: `/health` @@ -84,18 +56,26 @@ type Response struct { curl -i http://localhost:8000/health ``` -### `/api-spec/v1/verify` +## Example: `/api-spec/v1/verify` ```bash curl -i http://localhost:8000/api-spec/v1/verify ``` -Returns the checked-in bundled OpenAPI JSON artifact with `Content-Type: application/json`. +Returns the checked-in bundled OpenAPI JSON artifact with +`Content-Type: application/json`. -## Example: `/api/edu` (auth skipped locally) +## Example: `/api/v0/education-enrollments` ```bash -curl -i http://localhost:8000/api/edu +curl -i --request POST http://localhost:8000/api/v0/education-enrollments \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer ' \ + --data '{ + "firstName": "Lynette", + "lastName": "Oyola", + "dateOfBirth": "1988-10-24" + }' ``` ## Example: `/api/v0/veteran-disability-ratings` @@ -120,14 +100,18 @@ curl -i --request POST http://localhost:8000/api/v0/veteran-disability-ratings \ ## Current-State Caveats -- `/api/edu` currently does not accept caller-provided payload; it submits a hardcoded sample request from handler code. -- `main` now injects Redis into `api.New`, so the health route has the Redis client it expects. -- The intended public contract for this branch is versioned under `api-spec/v0/`, and the veteran disability route matches that contract while `/api/edu` remains a runtime-only scaffold. -- Error response bodies come from Fiber error handling and may be plain text. +- The v0 OpenAPI contract documents OAuth 2.0 client credentials, but the + runtime currently uses `SubjectMiddleware` and optional skip-auth locals + instead of a verifying auth middleware. +- Error responses come from Fiber error handling and are plain text unless a + handler returns JSON explicitly. +- The education and veteran handlers add metadata such as `apiVersion`, + `environment`, timestamps, and datasource duration to successful responses. ## Assumptions - **High confidence:** This page is a runtime reference, not the public API contract reference. -- **Medium confidence:** `/api/edu` will be removed or reshaped as the runtime - converges on the published v0 contract. +- **High confidence:** `POST /api/v0/education-enrollments` and + `POST /api/v0/veteran-disability-ratings` are the only current runtime POST + endpoints under `/api/v0/`. diff --git a/docs/architecture.md b/docs/architecture.md index 81769d4..243c11a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -48,7 +48,10 @@ flowchart LR ## Interfaces and Abstractions - `pkg/education/service.go` - - `type EducationService interface { Submit(ctx context.Context, req Request) (Response, error) }` + - `type Service interface { LookupEnrollmentStatus(ctx context.Context, req Request) (Response, error) }` + - `type HTTPTransport interface { Do(req *http.Request) (*http.Response, error) }` +- `pkg/veteran/service.go` + - `type Service interface { LookupDisabilityRating(ctx context.Context, req Request) (Response, error) }` - `type HTTPTransport interface { Do(req *http.Request) (*http.Response, error) }` - `pkg/circuitbreaker/circuitbreaker.go` - `type Breaker interface { Allow; OnSuccess; OnFailure }` @@ -62,7 +65,7 @@ without route-layer rewrites. - `runServer` starts `app.Listen` in a goroutine and selects on server error or signal context cancellation. - graceful shutdown uses `app.ShutdownWithTimeout(5 * time.Second)`. - Request lifecycle: - - handlers create per-request contexts with timeout (`/health`: 2s, `/api/edu`: 5s, `/api/v0/veteran-disability-ratings`: 5s). + - handlers create per-request contexts with timeout (`/health`: 2s, `POST /api/v0/education-enrollments`: 30s, `POST /api/v0/veteran-disability-ratings`: 5s). - Circuit-breaker middleware: - breaker registry map guarded with `sync.RWMutex`. - lazy breaker initialization via double-check lock pattern. @@ -80,10 +83,12 @@ without route-layer rewrites. Ordered middleware in `api.New`: -1. Recover -2. CORS (`*` origin/headers/methods) -3. Structured request logging (trace/span/request IDs) -4. Conditional Cognito auth middleware +1. Request ID context propagation +2. Structured request logging (trace/span/request IDs) +3. Recover +4. CORS (`*` origin/headers/methods) +5. Subject extraction middleware +6. Conditional local skip-auth identity injection ## Dependency Injection Pattern @@ -99,10 +104,14 @@ injects it. ## Technical Caveats (Current State) -- `/api/edu` handler builds a hardcoded request payload instead of binding user input. -- `/health` is registered before the auth middleware, so it remains a runtime-only unauthenticated health route. -- Current runtime routes are a mix of scaffold and contract-aligned paths: `GET /`, `GET /health`, `GET /api/edu`, and `POST /api/v0/veteran-disability-ratings`. -- `GET /api/edu` remains runtime scaffolding, while `POST /api/v0/veteran-disability-ratings` matches the checked-in v0 contract in `api-spec/v0/openapi.yaml`. +- `api.New` does not currently install a Cognito-verifying middleware. The + request subject comes from `X-Sub`, an unverified bearer token `sub` claim, + or skip-auth locals when `SKIP_AUTH=true`. +- `GET /health` and the two `POST /api/v0/*` verification routes are + circuit-breaker wrapped, but `GET /` and `GET /api-spec/v1/verify` are not. +- The public contract documents OAuth 2.0 client-credentials auth, while the + runtime currently focuses on subject propagation plus downstream provider + calls. - Some tests require local Redis and fail when unavailable. ## Assumptions diff --git a/docs/audit/2026-04-24_12-25-05.md b/docs/audit/2026-04-24_12-25-05.md new file mode 100644 index 0000000..d382c75 --- /dev/null +++ b/docs/audit/2026-04-24_12-25-05.md @@ -0,0 +1,179 @@ +# Documentation Accuracy Review (as of April 24, 2026) + +## Section A: Executive Summary + +Score: `94/100` + +Risk statement: Primary contributor-facing docs now match the current branch for +runtime routes, setup/config keys, and supported contract-tooling commands. The +remaining gap is architectural rather than editorial: the checked-in v0 +contract still documents OAuth 2.0 client-credentials auth while the running Go +app currently derives request identity from headers and bearer tokens without a +verifying auth middleware. + +Gate: `PASS` + +No meaningful unresolved `P1` or `P2` documentation-drift findings remain +after this audit/fix pass. + +## Section B: Severity-Ranked Findings + +No unresolved `P1` findings. + +No unresolved `P2` findings. + +| Severity | Doc location | Observed mismatch | Source-of-truth evidence on current branch | Recommended fix | +|---|---|---|---|---| +| `P3` | `docs/api.md:29`; `docs/api.md:103`; `docs/overview.md:109`; `docs/guides/02-authentication.md:5`; `docs/features/core/edu-openapi-spec.md:5` | The docs now accurately distinguish observed runtime behavior from the intended public contract, but readers still need both the runtime docs and the contract docs to understand the auth story because the implementation does not yet enforce the published OAuth scheme. | `api/app.go:101`; `api/app.go:105`; `api/middleware/middleware.go:28`; `api/middleware/middleware.go:66`; `api-spec/v0/openapi.yaml:18`; `api-spec/v0/openapi.yaml:27`; `api-spec/v0/openapi.yaml:117` | Keep the cross-links and caveats in place until runtime auth enforcement or contract changes remove the split-brain behavior. | + +## Section C: Update Backlog Checklist by Doc File + +### `README.md` + +- [x] Replaced missing direct OpenAPI helper-script references with supported + `mise` task guidance. +- [x] Added the list of direct helper scripts that actually exist in `scripts/`. + +### `api-spec/README.md` + +- [x] Replaced nonexistent shell-wrapper commands with supported `mise` and + `pnpm` workflows. + +### `docs/api.md` + +- [x] Rewrote the runtime endpoint table to match the live routes: + `GET /`, `GET /health`, `GET /api-spec/v1/verify`, + `POST /api/v0/education-enrollments`, and + `POST /api/v0/veteran-disability-ratings`. +- [x] Replaced stale Cognito-header claims with the current + `SubjectMiddleware` and skip-auth behavior. + +### `docs/overview.md` + +- [x] Updated the package inventory and request-flow diagram to match the + current route wiring and middleware stack. +- [x] Added an explicit note that the checked-in auth contract is ahead of + runtime enforcement. + +### `docs/architecture.md` + +- [x] Corrected interface names, middleware ordering, and handler timeout notes. +- [x] Removed stale `/api/edu` and Cognito-verifier claims. + +### `docs/setup.md` + +- [x] Corrected the compose-service description. +- [x] Fixed the environment-variable table to match + `pkg/core/config.go` defaults and supported keys. + +### `docs/features/README.md` + +- [x] Renamed the security entry to reflect the current runtime subject-context + behavior instead of nonexistent Cognito verification middleware. + +### `docs/features/security/cognito-auth.md` + +- [x] Re-scoped the page to current request-subject propagation behavior. + +### `docs/features/core/nsc-education.md` + +- [x] Removed the stale claim that the HTTP handler still uses a hardcoded + request payload. + +### `docs/features/infrastructure/redis.md` + +- [x] Removed the stale nil-Redis wiring caveat. + +### `docs/guides/01-getting-started.md` + +- [x] Corrected the versioned education path to + `POST /api/v0/education-enrollments`. +- [x] Fixed the broken local examples link and `client_secret` typo. + +### `docs/guides/02-authentication.md` + +- [x] Added an explicit runtime-vs-contract caveat for auth behavior. + +### `docs/guides/03-usage-examples.md` + +- [x] Fixed minor `API` capitalization typos. + +### `docs/planning/public-api-data-model-design-first.md` + +- [x] Replaced missing direct script invocations with supported `mise` and + `pnpm` contract-tooling commands. + +### `AGENTS.md` + +- [x] Corrected the observed OpenAPI entry point path to + `api-spec/v0/openapi.yaml`. + +## Section D: Hygiene Appendix + +### Command Log + +```text +git status --short --branch +git log --oneline -n 10 +rg --files docs | sort +sed -n '1,260p' README.md +sed -n '1,260p' docs/setup.md +sed -n '1,260p' docs/api.md +sed -n '1,260p' docs/overview.md +sed -n '1,260p' docs/architecture.md +sed -n '1,260p' api-spec/README.md +sed -n '1,260p' AGENTS.md +sed -n '1,260p' docs/guides/01-getting-started.md +sed -n '1,260p' docs/guides/02-authentication.md +sed -n '1,260p' docs/guides/03-usage-examples.md +sed -n '1,260p' docs/features/core/nsc-education.md +sed -n '1,260p' docs/features/security/cognito-auth.md +sed -n '1,260p' docs/features/infrastructure/redis.md +sed -n '1,260p' docs/planning/public-api-data-model-design-first.md +sed -n '1,260p' pkg/core/config.go +sed -n '1,260p' api/app.go +sed -n '1,260p' api/routes/router.go +sed -n '1,260p' api/routes/status_router.go +sed -n '1,260p' api/middleware/middleware.go +sed -n '1,260p' api/handlers/education_handler.go +sed -n '1,260p' api/handlers/veteran_handler.go +sed -n '1,220p' api/handlers/openapi_spec_handler.go +sed -n '1,220p' api/handlers/status_handler.go +sed -n '1,260p' mise.toml +markdownlint-cli2 README.md AGENTS.md api-spec/README.md schema/README.md "docs/**/*.md" +zsh -lc 'setopt null_glob; for f in README.md AGENTS.md api-spec/README.md schema/README.md docs/**/*.md; do perl -ne ...; done' +rg -n --hidden --glob '.git' --glob '!docs/audit/**' --glob '!public.jwk' "BEGIN (RSA|EC|OPENSSH|PRIVATE) KEY|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{35}" README.md AGENTS.md api-spec docs schema scripts pkg api main.go .env.example +``` + +### Sensitive-File Scan + +- `gitleaks` is not installed in this worktree, so the repository-local secret + scan fell back to a tracked-file regex sweep for common key/token patterns. +- Result: no matches in the reviewed documentation, config, or source files. + +### Markdown Validation + +- `markdownlint-cli2 README.md AGENTS.md api-spec/README.md schema/README.md "docs/**/*.md"` +- Result: `0 error(s)` + +### Link and Path Validation + +- Relative markdown-link validation across reviewed docs completed with no + missing local targets. + +### Hygiene Verdict + +- `PASS` + +## Section E: Deferred Watchlist (Non-blocking) + +- If runtime auth enforcement is added back to `api.New`, update + `docs/api.md`, `docs/overview.md`, and + `docs/features/security/cognito-auth.md` so the implementation and contract + narratives can converge again. +- If the OpenAPI contract starts documenting non-2xx error envelopes, update + `docs/features/core/edu-openapi-spec.md` and the public guides to include the + final error model. +- If `gitleaks` becomes available in the local toolchain or CI image, replace + the regex fallback in future audit runs with the configured + `.github/.gitleaks.toml` scan. diff --git a/docs/features/README.md b/docs/features/README.md index a5813bf..ce14563 100644 --- a/docs/features/README.md +++ b/docs/features/README.md @@ -22,7 +22,7 @@ controls, and resilience patterns without scanning a single flat list. | Component | Purpose | Functionality | |---|---|---| -| [Cognito Auth](security/cognito-auth.md) | Document Cognito access-token validation middleware. | Covers token header/claims checks, local context propagation, and auth-related edge cases. | +| [Request Subject Context](security/cognito-auth.md) | Document how the runtime derives request identity today. | Covers `X-Sub`, bearer-token `sub` parsing, skip-auth locals, and the gap between runtime behavior and the checked-in OAuth contract. | ## Resilience diff --git a/docs/features/core/nsc-education.md b/docs/features/core/nsc-education.md index 94f1644..b1fc2d9 100644 --- a/docs/features/core/nsc-education.md +++ b/docs/features/core/nsc-education.md @@ -2,11 +2,13 @@ ## Feature Overview -Provides education verification through NSC by submitting a structured JSON request with OAuth2 client-credentials authentication. +Provides education verification through NSC by accepting a caller JSON payload +and submitting a structured upstream request with OAuth2 client-credentials +authentication. ## Business Logic -- Build `education.Request` payload. +- Parse the request body into `education.Request`. - Serialize request body to JSON. - Create HTTP POST to `NSC_SUBMIT_URL`. - Use OAuth2-enabled HTTP client sourced from NSC token endpoint. @@ -60,11 +62,12 @@ if err != nil { ## Future Improvements -- Accept caller-provided request payload in HTTP handler. -- Add validation for required request fields before submit. +- Add richer request validation beyond the current required + `firstName`/`lastName`/`dateOfBirth` checks. - Introduce retry policy with bounded backoff for transient 5xx errors. - Add contract tests against NSC sandbox with fixtures. ## Assumptions -- **Medium confidence:** Current handler payload is a scaffold for integration testing, not final business API behavior. +- **High confidence:** The HTTP handler now accepts caller-provided payloads and + is no longer the earlier hardcoded-request scaffold. diff --git a/docs/features/infrastructure/redis.md b/docs/features/infrastructure/redis.md index d0bbedd..c6ea82b 100644 --- a/docs/features/infrastructure/redis.md +++ b/docs/features/infrastructure/redis.md @@ -74,8 +74,10 @@ if err := redisotel.InstrumentMetrics(rdb); err != nil { ## Assumptions -- **High confidence:** Redis is an operational dependency for current startup and status-check behavior. -- **High confidence:** There is an active wiring caveat on `main` where status route setup may receive nil Redis via `api.New` config path until Redis injection is corrected there. +- **High confidence:** Redis is an operational dependency for current startup + and status-check behavior. +- **High confidence:** `main` now injects the Redis client into `api.New` and + route registration before startup begins serving requests. --- diff --git a/docs/features/security/cognito-auth.md b/docs/features/security/cognito-auth.md index 08f79e6..edb91c0 100644 --- a/docs/features/security/cognito-auth.md +++ b/docs/features/security/cognito-auth.md @@ -1,16 +1,18 @@ -# Feature: Cognito Auth +# Feature: Request Subject Context ## Feature Overview -Validates AWS Cognito access tokens for incoming requests when auth is enabled. +Captures a request subject for logging, reporting, and handler context. ## Business Logic -- Read token from `x-amzn-oidc-accesstoken`. -- Load JWKS from Cognito issuer URL. -- Parse and validate JWT claims/signature. -- Enforce `client_id` match with configured app client. -- Add selected claims (`sub`, `username`, `scope`, `groups`) to Fiber locals. +- If `SkipAuthMiddleware` already ran, preserve the injected local identity. +- Otherwise read `X-Sub` when present. +- Otherwise parse the `Authorization: Bearer ` value without signature + verification and copy the JWT `sub` claim. +- Fall back to `unknown-subject` when neither source is available. +- When `SKIP_AUTH=true`, allow local override headers such as + `x-skip-auth-sub` and `x-skip-auth-groups`. ## Package Location @@ -19,45 +21,50 @@ Validates AWS Cognito access tokens for incoming requests when auth is enabled. ## Key Structs and Interfaces -- `CognitoConfig` -- `CognitoVerifier` -- `NewCognitoVerifier` -- `FiberMiddleware` +- `SubjectMiddleware` +- `SkipAuthMiddleware` +- `parseGroups` ## Real Code Excerpt ```go -tok, err := jwt.Parse( - []byte(raw), - jwt.WithKeySet(keyset), - jwt.WithValidate(true), - jwt.WithIssuer(v.issuer), - jwt.WithClaimValue("token_use", "access"), -) -if err != nil { - return fiber.ErrUnauthorized +auth := c.Get(fiber.HeaderAuthorization) +if strings.HasPrefix(auth, "Bearer ") { + tokenString := strings.TrimPrefix(auth, "Bearer ") + token, err := jwt.ParseString(tokenString, jwt.WithVerify(false), jwt.WithValidate(false)) + if err != nil { + return fiber.ErrUnauthorized + } + sub = token.Subject() } ``` ## Edge Cases Handled Today -- Missing token header returns `401`. -- JWKS retrieval failures return unauthorized error. -- Invalid or mismatched `client_id` returns `401`. -- Config validation blocks startup if required cognito settings are missing. +- Missing headers fall back to `unknown-subject`. +- Malformed bearer tokens return `401`. +- `SkipAuthMiddleware` injects stable local defaults when auth is disabled. +- Comma-separated `x-skip-auth-groups` values are trimmed and normalized. ## Performance and Operational Considerations -- JWKS uses `jwk.Cache` to avoid repeated key fetches. -- Request-time auth check includes a 5-second context timeout. -- Middleware is globally applied unless `SKIP_AUTH=true`. +- Subject extraction is cheap and local; no JWKS fetch or external auth call is + performed in the current branch. +- Middleware is globally applied so reporting always has a `client_id`-like + value to log, even if it falls back to `unknown-subject`. +- This behavior does not enforce the OAuth 2.0 client-credentials contract + described in `api-spec/v0/openapi.yaml`. ## Future Improvements -- Add explicit middleware unit/integration tests. -- Support configurable token header name for proxy variations. -- Improve unauthorized response detail for operator troubleshooting while preserving security posture. +- Reintroduce a verifying auth middleware if the runtime is expected to enforce + the checked-in OAuth contract. +- Add explicit docs once token verification, header semantics, and local-dev + behavior are finalized. +- Keep security feature docs aligned with the runtime and spec separately + instead of merging the two concerns. ## Assumptions -- **High confidence:** Current claim checks are intentionally minimal and focused on access-token validity plus client binding. +- **High confidence:** The current middleware stack propagates request identity + but does not validate external tokens beyond parsing a bearer token for `sub`. diff --git a/docs/guides/01-getting-started.md b/docs/guides/01-getting-started.md index 3f4f7b3..9b7a237 100644 --- a/docs/guides/01-getting-started.md +++ b/docs/guides/01-getting-started.md @@ -42,7 +42,7 @@ To begin using the Emmy API, we will start with manual steps which ensure your c To **authenticate** with the Emmy API, you will use your `` and `` to obtain a token for all Emmy API operations. Once you obtain a token, you will use it to make all subsequent Emmy API requests. Tokens are short-lived and expire, typically after 60 minutes. You can use this token as many times as you'd like until expiration, or you can generate a new token each time. -Obtaining a token requires that you use a **Basic Authentication** HTTP header. This means that you will join your `` and `` with a colon `:` and then Base64 encode the entire string. Follow the [Authentication Base64 Encoding](02-authentication.md#credential-format) instructions to build your credential string. We'll refer to the credential string that you generate as ``. +Obtaining a token requires that you use a **Basic Authentication** HTTP header. This means that you will join your `` and `` with a colon `:` and then Base64 encode the entire string. Follow the [Authentication Guide](02-authentication.md) for token-request details. We'll refer to the credential string that you generate as ``. For simplicity, we'll use `curl` to showcase getting an access token. From a terminal or command prompt, run this `curl` command ([how do I install `curl`?](../examples/v0/curl.md#installing-curl)) by substituting the appropriate variables (shown `````` below) with their actual values: @@ -71,9 +71,9 @@ _(Review the [Authentication Guide](02-authentication.md) for more details on au ### Step 2: Prepare your Request Payload -Requests to the Emmy API use JSON content payloads in the request body. First, review the [Emmy API specification](https://cmsgov.github.io/emmy-api/swagger-ui) for the operation you would like to perform. In this example, we will make request a member's educational enrollment information from the Emmy API's `/v0/education-enrollments` endpoint. +Requests to the Emmy API use JSON content payloads in the request body. First, review the [Emmy API specification](https://cmsgov.github.io/emmy-api/swagger-ui) for the operation you would like to perform. In this example, we will request a member's educational enrollment information from the Emmy API's `/api/v0/education-enrollments` endpoint. -In the [API specs for `/v0/education-enrollments`](https://cmsgov.github.io/emmy-api/swagger-ui/#/Education%20Verification/getEducationEnrollmentStatus), you can see the structure of the request body. Build a JSON body using your member's information, like so: +In the [API specs for `/api/v0/education-enrollments`](https://cmsgov.github.io/emmy-api/swagger-ui/#/Education%20Verification/getEducationEnrollmentStatus), you can see the structure of the request body. Build a JSON body using your member's information, like so: ```json { @@ -90,12 +90,12 @@ With your JSON payload prepared, you can now [make a request to the Emmy API](01 ### Step 3: Invoke an Emmy API Request -In this step, we will again use the `curl` tool to make a request to the Emmy API. We will continue the example from step 2 and request member information from the [Education `/v0/education-enrollments`](https://cmsgov.github.io/emmy-api/swagger-ui/#/Education%20Verification/getEducationEnrollmentStatus) endpoint. +In this step, we will again use the `curl` tool to make a request to the Emmy API. We will continue the example from step 2 and request member information from the [Education `/api/v0/education-enrollments`](https://cmsgov.github.io/emmy-api/swagger-ui/#/Education%20Verification/getEducationEnrollmentStatus) endpoint. This request is slightly more complex since it now has HTTP authorization, a content body, and a different endpoint. Use this example by substituting the values for your environment: ```bash -curl --location --request POST '/v0/education-enrollments' \ +curl --location --request POST '/api/v0/education-enrollments' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ --data '{ @@ -113,4 +113,4 @@ After a moment, the Emmy API will provide a verification response for the applic Now that you have completed an end-to-end test of using the Emmy API, you can use these lessons learned to connect to and use the API in your own application. -- [Visit the Emmy API examples](../../examples/README.md) +- [Visit the Emmy API examples](03-usage-examples.md) diff --git a/docs/guides/02-authentication.md b/docs/guides/02-authentication.md index 422d5ff..3289d74 100644 --- a/docs/guides/02-authentication.md +++ b/docs/guides/02-authentication.md @@ -6,6 +6,11 @@ The v0 contract uses the `OAuth2ClientCredentials` security scheme. Clients obtain an access token from the configured token endpoint and then present that token as a bearer token on API requests. +The current Go runtime docs in [`docs/api.md`](../api.md) should be read +alongside this guide: the checked-in v0 contract documents OAuth, while the +runtime currently extracts request identity from headers and bearer tokens +without enforcing the full contract server-side. + For the checked-in v0 spec, the security scheme is defined in `api-spec/v0/openapi.yaml` with this token URL: diff --git a/docs/guides/03-usage-examples.md b/docs/guides/03-usage-examples.md index 71629b3..4419fc2 100644 --- a/docs/guides/03-usage-examples.md +++ b/docs/guides/03-usage-examples.md @@ -25,7 +25,7 @@ Example success response: } ``` -View [education response detail](./04-education-responses-detail.md) for more examples and further nuance of the APi explained. +View [education response detail](./04-education-responses-detail.md) for more examples and further nuance of the API explained. ## Veteran Disability Verification @@ -50,4 +50,4 @@ Example success response: } ``` -View [veteran disability response detail](./05-veteran-responses-detail.md) for more examples and further nuance of the APi explained. +View [veteran disability response detail](./05-veteran-responses-detail.md) for more examples and further nuance of the API explained. diff --git a/docs/overview.md b/docs/overview.md index 92ae1e0..be9c01c 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -3,84 +3,83 @@ ## Purpose The Verification Service API provides a unified HTTP interface for eligibility -verification workflows, currently focused on a runtime education scaffold, a -Redis-backed health check, and the checked-in v0 veteran disability contract. +verification workflows. The current branch exposes a Redis-backed health check, +two versioned verification endpoints, and a route that serves the bundled v0 +OpenAPI artifact. -This service evolved from consent-based verification work and is intended to -reduce manual burden during benefits eligibility evaluation. - -The intended public API contract for this branch is defined in +The intended public contract for this branch is defined in `api-spec/v0/openapi.yaml` and the reusable schemas in `schema/v0/`. This page -describes the current repository and runtime shape, which still contains a mix -of contract-aligned routes and implementation scaffolding. +describes the current repository and runtime shape, including places where the +implementation has not yet caught up to the documented contract. ## System Context -Runtime dependencies in current implementation: +Runtime dependencies in the current implementation: -- Fiber (`github.com/gofiber/fiber/v2`) for HTTP server and routing. -- Redis (`github.com/redis/go-redis/v9`) for health checks and distributed circuit-breaker state. -- NSC endpoints (`NSC_TOKEN_URL`, `NSC_SUBMIT_URL`) for education verification. -- AWS Cognito JWKS/JWT validation for request authentication (when `SKIP_AUTH=false`). -- Datadog Orchestrion for build-time automatic instrumentation. +- Fiber (`github.com/gofiber/fiber/v2`) for HTTP server and routing +- Redis (`github.com/redis/go-redis/v9`) for startup health checks and + distributed circuit-breaker state +- NSC endpoints (`NSC_TOKEN_URL`, `NSC_SUBMIT_URL`) for education verification +- VA endpoints (`VA_TOKEN_URL`, `VA_BASE_URL`) for veteran disability lookups +- Datadog Orchestrion for build-time automatic instrumentation ## Key Packages -- `main`: process bootstrap, env/config load, Redis client init, route registration, graceful shutdown. -- `api`: Fiber app construction and shared middleware setup. -- `api/routes`: endpoint registration (`/`, `/health`, `/api/edu`, `/api/v0/veteran-disability-ratings`). -- `api/handlers`: HTTP handlers for Redis health, education scaffolding, and veteran verification. -- `api/middleware`: Cognito auth and circuit-breaker middleware. -- `pkg/core`: configuration, logger. -- `pkg/education`: NSC service abstraction and HTTP/OAuth submit flow. -- `pkg/veteran`: VA service abstraction and JWT client-assertion flow. -- `pkg/circuitbreaker`: Redis-backed circuit-breaker implementation. -- `pkg/redis`: Redis client factory and health ping. +- `main`: process bootstrap, env/config load, Redis init, route registration, + graceful shutdown +- `api`: Fiber app construction and shared middleware setup +- `api/routes`: endpoint registration (`/`, `/health`, `/api-spec/v1/verify`, + `POST /api/v0/education-enrollments`, + `POST /api/v0/veteran-disability-ratings`) +- `api/handlers`: HTTP handlers for Redis health, bundled OpenAPI serving, + education verification, and veteran verification +- `api/middleware`: request subject extraction, local skip-auth identity, and + circuit-breaker middleware +- `pkg/core`: configuration and logger setup +- `pkg/education`: NSC service abstraction and HTTP/OAuth submit flow +- `pkg/veteran`: VA service abstraction and JWT client-assertion flow +- `pkg/circuitbreaker`: Redis-backed circuit-breaker implementation +- `pkg/redis`: Redis client factory and health ping helper ## Design Principles (Observed) -- Explicit startup configuration from environment via `core.NewConfigFromEnv()`. -- Interface-driven boundaries for integration points (`EducationService`, `HTTPTransport`, `Breaker`). -- Middleware-first cross-cutting concerns (recovery, CORS, request logging, auth, circuit breaking). -- Operational defaults favoring availability in unknown breaker state (`FailOpen=true` by default). +- Explicit startup configuration from environment via `core.NewConfigFromEnv()` +- Interface-driven boundaries for integration points (`Service`, + `HTTPTransport`, `Breaker`) +- Middleware-first cross-cutting concerns for request IDs, logging, panic + recovery, CORS, subject propagation, and breaker checks +- Operational defaults favoring availability in unknown breaker state + (`FailOpen=true`) ## High-Level Request Flow ```mermaid flowchart TD A[Client] --> B[Fiber App] - B --> C[Recover + CORS + Slog middleware] - C --> D{SKIP_AUTH == false?} - D -->|Yes| E[Cognito JWT Verifier] - D -->|No| F[Route Handler] - E --> F - - F --> G{Circuit Breaker Allow?} - G -->|No| H[503 Service Unavailable] - - G -->|Yes: /health| I[Redis Ping] - I --> J[200 OK or Fiber Error] - - G -->|Yes: /api/edu| K[EducationService.Submit] - K --> L[OAuth2 client credentials token] - L --> M[NSC submit endpoint] - M --> N[JSON response] - - G -->|Yes: /api/v0/veteran-disability-ratings| V[VeteranService.LookupDisabilityRating] - V --> W[VA token exchange] - W --> X[VA disability endpoint] - X --> Y[JSON response] - - B -.-> O[Datadog Agent] - I -.-> O - K -.-> O - V -.-> O + B --> C[Request ID + request logging] + C --> D[Recover + CORS] + D --> E[Subject middleware] + E --> F{SKIP_AUTH == true?} + F -->|Yes| G[Inject local skip-auth identity] + F -->|No| H[Keep observed subject] + G --> I{Circuit breaker route?} + H --> I + + I -->|No: / or /api-spec/v1/verify| J[Route handler] + I -->|Yes: /health| K[Redis Ping] + I -->|Yes: /api/v0/education-enrollments| L[EducationService.LookupEnrollmentStatus] + I -->|Yes: /api/v0/veteran-disability-ratings| M[VeteranService.LookupDisabilityRating] + I -->|Breaker denied| N[503 Service Unavailable] + + L --> O[NSC OAuth token + submit] + M --> P[VA token exchange + disability endpoint] + + B -.-> Q[Datadog Agent] + K -.-> Q + O -.-> Q + P -.-> Q ``` -Current wiring caveat on `main`: `api.New` now receives a Redis client from -`main`, so `/health` can use the same Redis dependency that powers the breaker -and health checks. - ## Documentation Map - [Architecture](architecture.md) @@ -102,11 +101,10 @@ Initial requirements referenced `/docs/planing`; this repo standardizes on ## Assumptions -- **High confidence:** Redis is the only persistent/shared runtime store - currently used by this service. -- **High confidence:** `/api/edu` is presently implementation scaffolding and - should not be treated as the public contract for this branch. -- **High confidence:** `POST /api/v0/veteran-disability-ratings` is the current - checked-in v0 contract path for veteran verification. -- **Medium confidence:** Additional verification domains beyond the current - runtime routes may be introduced in future versions. +- **High confidence:** Redis is the only shared runtime store currently used by + this service. +- **High confidence:** The versioned verification endpoints in the current + branch are `POST /api/v0/education-enrollments` and + `POST /api/v0/veteran-disability-ratings`. +- **Medium confidence:** The checked-in v0 auth contract is ahead of runtime + enforcement and should be read alongside [Runtime API Notes](api.md). diff --git a/docs/planning/public-api-data-model-design-first.md b/docs/planning/public-api-data-model-design-first.md index 2fc14ce..704fafb 100644 --- a/docs/planning/public-api-data-model-design-first.md +++ b/docs/planning/public-api-data-model-design-first.md @@ -161,23 +161,21 @@ service behavior or superseded response envelopes. ```bash # 1. Bundle the checked-in YAML and JSON artifacts (required for multi-file refs) -./scripts/bundle-api-spec +mise run bundle-api-spec # 2. Validate OpenAPI structure -./scripts/validate-api-spec +mise run validate-api-spec # 3. Lint style and governance rules -./scripts/lint-api-spec +mise run lint-api-spec ``` -Using `mise`, the same workflow is available as: +Equivalent `pnpm` commands are: ```bash -mise install -mise run bundle-api-spec -mise run validate-api-spec -mise run lint-api-spec -mise run check-api-spec +pnpm run bundle:api-spec +pnpm run validate:api-spec +pnpm run lint:api-spec ``` Starter `.spectral.yaml` (commit and version in repo): diff --git a/docs/setup.md b/docs/setup.md index 49682fc..dbbd555 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -4,7 +4,7 @@ - Go `1.25.x` (`go.mod` sets `go 1.25`). - Docker and Docker Compose for containerized local workflows. The committed - compose file currently provides API and observability services only. + compose file currently provides API, Redis, and observability services. - Local Redis at `localhost:6379` for runtime health checks and several tests. ## Environment Variables @@ -14,11 +14,10 @@ | Category | Variables | Defaults | |---|---|---| | Service | `ENVIRONMENT`, `PORT`, `SKIP_AUTH` | `development`, `3000`, `false` | -| Orchestrion | `DD_AGENT_HOST`, `DD_TRACE_AGENT_PORT` | `localhost`, `8126` | -| Cognito | `COGNITO_REGION`, `COGNITO_USER_POOL_ID`, `COGNITO_APP_CLIENT_ID` | `us-east-1`, `UNSET`, `UNSET` | | Redis | `REDIS_ADDR`, `REDIS_PASSWORD`, `REDIS_DB`, `REDIS_USE_TLS`, `REDIS_INSECURE_SKIP_VERIFY` | `localhost:6379`, empty, `0`, `true`, `false` | | NSC | `NSC_SUBMIT_URL`, `NSC_TOKEN_URL`, `NSC_CLIENT_SECRET`, `NSC_CLIENT_ID`, `NSC_ACCOUNT_ID` | empty | | VA | `VA_BASE_URL`, `VA_TOKEN_URL`, `VA_CLIENT_ID`, `VA_AUD`, `VA_PRIVATE_KEY_PATH`, `VA_TIMEOUT_SECONDS` | empty, empty, empty, empty, empty, `5` | +| Reporting | `SQS_QUEUE_URL`, `SERVICE_VERSION` | empty, `1.3.0` | - The table above reflects code defaults from `pkg/core/config.go`. - `.env.example` overrides the local example port to `PORT=8000` and includes