From f8d213564e787abd7fef38081f694a540ed16082 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sun, 28 Jun 2026 04:22:21 -0400 Subject: [PATCH 1/3] fix(notary): document admin posture and config-apply 503 in OpenAPI The runtime serves GET /admin/v1/posture and a 503 config.apply_unavailable response on /admin/v1/config/apply, but the generated OpenAPI document omitted both, so the published contract did not match runtime. Register /admin/v1/posture (tier query parameter; 200 plus the 400/401/403/500/503 problem responses) and add the 503 problem response to /admin/v1/config/apply. Each problem example mirrors the exact runtime body and application/problem+json media type. Regenerate the committed artifact, preserving the pinned product version 0.6.2 (the generator emits the crate version via CARGO_PKG_VERSION; the published product/docset version is 0.6.2). Signed-off-by: Jeremi Joslin --- crates/registry-notary-server/src/openapi.rs | 135 ++++++++++++++++- .../openapi/registry-notary.openapi.json | 141 ++++++++++++++++++ 2 files changed, 275 insertions(+), 1 deletion(-) diff --git a/crates/registry-notary-server/src/openapi.rs b/crates/registry-notary-server/src/openapi.rs index ecbee1a6..c28cc7bd 100644 --- a/crates/registry-notary-server/src/openapi.rs +++ b/crates/registry-notary-server/src/openapi.rs @@ -98,6 +98,34 @@ fn build_openapi_document() -> Value { } } }, + "/admin/v1/posture": { + "get": { + "summary": "Read redacted runtime posture", + "description": "Returns redacted runtime posture for the requested tier. The response body is a registry.ops.posture.v1 document describing instance, configuration, notary, deployment, and audit posture.", + "operationId": "adminPosture", + "security": [ + { "apiKeyAuth": [] }, + { "bearerAuth": [] } + ], + "parameters": [ + { + "name": "tier", + "in": "query", + "required": false, + "description": "Redaction tier for the posture document. Defaults to default.", + "schema": { "type": "string", "enum": ["default", "restricted"] } + } + ], + "responses": { + "200": { "description": "Redacted posture for the requested tier" }, + "400": { "description": "Posture tier is invalid" }, + "401": { "description": "Missing or invalid credential" }, + "403": { "description": "Caller lacks registry_notary:ops_read scope" }, + "500": { "description": "Posture could not be filtered for the requested tier" }, + "503": { "description": "Posture state is unavailable" } + } + } + }, "/admin/v1/config/verify": { "post": { "summary": "Validate a candidate runtime config", @@ -149,7 +177,8 @@ fn build_openapi_document() -> Value { "409": { "description": "Candidate config requires restart and was not applied" }, "400": { "description": "Candidate config is invalid" }, "401": { "description": "Missing or invalid credential" }, - "403": { "description": "Caller lacks registry_notary:admin scope" } + "403": { "description": "Caller lacks registry_notary:admin scope" }, + "503": { "description": "Config apply is unavailable" } } } }, @@ -1275,6 +1304,79 @@ fn add_response_examples(document: &mut Value) { ), ); } + set_problem_response( + document, + "/admin/v1/config/apply", + "post", + "503", + "Config apply is unavailable", + problem_example( + 503, + "config.apply_unavailable", + "Config apply unavailable", + "config_trust.antirollback_state_path is not configured", + ), + ); + set_problem_response( + document, + "/admin/v1/posture", + "get", + "400", + "Posture tier is invalid", + admin_error_example( + 400, + "registry.admin.posture.invalid_tier", + "Admin posture tier invalid", + "posture tier must be default or restricted", + ), + ); + set_problem_response( + document, + "/admin/v1/posture", + "get", + "401", + "Missing or invalid credential", + missing_credential_example(), + ); + set_problem_response( + document, + "/admin/v1/posture", + "get", + "403", + "Caller lacks registry_notary:ops_read scope", + problem_example( + 403, + "auth.scope_denied", + "Scope denied", + "missing required scope", + ), + ); + set_problem_response( + document, + "/admin/v1/posture", + "get", + "500", + "Posture could not be filtered for the requested tier", + problem_example( + 500, + "posture.filter_failed", + "Admin posture unavailable", + "admin posture could not be filtered for the requested tier", + ), + ); + set_problem_response( + document, + "/admin/v1/posture", + "get", + "503", + "Posture state is unavailable", + admin_error_example( + 503, + "posture.unavailable", + "Admin posture unavailable", + "posture state is unavailable", + ), + ); set_json_response( document, "/openapi.json", @@ -3850,6 +3952,7 @@ mod tests { "/admin/v1/config/verify", "/admin/v1/config/dry-run", "/admin/v1/config/apply", + "/admin/v1/posture", "/openapi.json", "/.well-known/evidence-service", "/.well-known/evidence/jwks.json", @@ -3877,6 +3980,30 @@ mod tests { } } + #[test] + fn documents_admin_posture_query_and_response_contract() { + let doc = openapi_document(); + let posture = &doc["paths"]["/admin/v1/posture"]["get"]; + assert!(posture.is_object(), "admin posture GET is documented"); + // Authenticated read: posture must not be exposed as a public route. + assert_eq!( + posture["security"], + json!([{ "apiKeyAuth": [] }, { "bearerAuth": [] }]) + ); + // The tier query parameter is constrained to the runtime-accepted values. + let tier = &posture["parameters"][0]; + assert_eq!(tier["name"], json!("tier")); + assert_eq!(tier["in"], json!("query")); + assert_eq!(tier["schema"]["enum"], json!(["default", "restricted"])); + // Every response the handler can return is documented. + for status in ["200", "400", "401", "403", "500", "503"] { + assert!( + posture["responses"][status].is_object(), + "posture documents the {status} response" + ); + } + } + #[test] fn document_info_tracks_crate_metadata() { let doc = serde_json::to_value(openapi_document()).expect("document serializes"); @@ -4154,6 +4281,12 @@ mod tests { ("/admin/v1/config/apply", "post", "400"), ("/admin/v1/config/apply", "post", "401"), ("/admin/v1/config/apply", "post", "403"), + ("/admin/v1/config/apply", "post", "503"), + ("/admin/v1/posture", "get", "400"), + ("/admin/v1/posture", "get", "401"), + ("/admin/v1/posture", "get", "403"), + ("/admin/v1/posture", "get", "500"), + ("/admin/v1/posture", "get", "503"), ("/.well-known/evidence-service", "get", "401"), ("/v1/claims", "get", "401"), ("/v1/claims/{claim_id}", "get", "401"), diff --git a/products/notary/openapi/registry-notary.openapi.json b/products/notary/openapi/registry-notary.openapi.json index 7f890a8d..dbacf798 100644 --- a/products/notary/openapi/registry-notary.openapi.json +++ b/products/notary/openapi/registry-notary.openapi.json @@ -2358,6 +2358,23 @@ } }, "description": "Candidate config requires restart and was not applied" + }, + "503": { + "content": { + "application/problem+json": { + "example": { + "code": "config.apply_unavailable", + "detail": "config_trust.antirollback_state_path is not configured", + "status": 503, + "title": "Config apply unavailable", + "type": "https://docs.registry-notary.dev/problems/config/apply_unavailable" + }, + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Config apply is unavailable" } }, "security": [ @@ -2700,6 +2717,130 @@ "summary": "Update credential lifecycle status" } }, + "/admin/v1/posture": { + "get": { + "description": "Returns redacted runtime posture for the requested tier. The response body is a registry.ops.posture.v1 document describing instance, configuration, notary, deployment, and audit posture.", + "operationId": "adminPosture", + "parameters": [ + { + "description": "Redaction tier for the posture document. Defaults to default.", + "in": "query", + "name": "tier", + "required": false, + "schema": { + "enum": [ + "default", + "restricted" + ], + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Redacted posture for the requested tier" + }, + "400": { + "content": { + "application/problem+json": { + "example": { + "code": "registry.admin.posture.invalid_tier", + "detail": "posture tier must be default or restricted", + "message": "posture tier must be default or restricted", + "schema": "registry.admin.error.v1", + "status": 400, + "title": "Admin posture tier invalid", + "type": "https://docs.registry-notary.dev/problems/registry/admin/posture/invalid_tier" + }, + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Posture tier is invalid" + }, + "401": { + "content": { + "application/problem+json": { + "example": { + "code": "auth.missing_credential", + "detail": "missing authentication credential", + "status": 401, + "title": "Missing credential", + "type": "https://docs.registry-notary.dev/problems/auth/missing_credential" + }, + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Missing or invalid credential" + }, + "403": { + "content": { + "application/problem+json": { + "example": { + "code": "auth.scope_denied", + "detail": "missing required scope", + "status": 403, + "title": "Scope denied", + "type": "https://docs.registry-notary.dev/problems/auth/scope_denied" + }, + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Caller lacks registry_notary:ops_read scope" + }, + "500": { + "content": { + "application/problem+json": { + "example": { + "code": "posture.filter_failed", + "detail": "admin posture could not be filtered for the requested tier", + "status": 500, + "title": "Admin posture unavailable", + "type": "https://docs.registry-notary.dev/problems/posture/filter_failed" + }, + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Posture could not be filtered for the requested tier" + }, + "503": { + "content": { + "application/problem+json": { + "example": { + "code": "posture.unavailable", + "detail": "posture state is unavailable", + "message": "posture state is unavailable", + "schema": "registry.admin.error.v1", + "status": 503, + "title": "Admin posture unavailable", + "type": "https://docs.registry-notary.dev/problems/posture/unavailable" + }, + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Posture state is unavailable" + } + }, + "security": [ + { + "apiKeyAuth": [] + }, + { + "bearerAuth": [] + } + ], + "summary": "Read redacted runtime posture" + } + }, "/admin/v1/reload": { "post": { "description": "Standalone mode does not support runtime configuration reload. Operators should call /admin/v1/capabilities before invoking product-specific reload operations.", From 375ca437076c62082e2148552f95ae493dfaf647 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sun, 28 Jun 2026 06:19:27 -0400 Subject: [PATCH 2/3] fix(notary): document posture 200 response body in OpenAPI The GET /admin/v1/posture 200 response only had a description, so spec consumers would generate the operation as returning no body even though the handler returns a registry.ops.posture.v1 document. Add an application/json example sourced from registry-platform-ops NOTARY_POSTURE_EXAMPLE_V1 (the committed schema-valid sample), mirroring the admin capabilities pattern. Addresses Codex review feedback on #168. Signed-off-by: Jeremi Joslin --- crates/registry-notary-server/src/openapi.rs | 15 +++ .../openapi/registry-notary.openapi.json | 97 +++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/crates/registry-notary-server/src/openapi.rs b/crates/registry-notary-server/src/openapi.rs index c28cc7bd..e3352fb3 100644 --- a/crates/registry-notary-server/src/openapi.rs +++ b/crates/registry-notary-server/src/openapi.rs @@ -1317,6 +1317,15 @@ fn add_response_examples(document: &mut Value) { "config_trust.antirollback_state_path is not configured", ), ); + set_json_response( + document, + "/admin/v1/posture", + "get", + "200", + "Redacted posture for the requested tier", + serde_json::from_str(registry_platform_ops::NOTARY_POSTURE_EXAMPLE_V1) + .expect("notary posture example is valid JSON"), + ); set_problem_response( document, "/admin/v1/posture", @@ -4002,6 +4011,11 @@ mod tests { "posture documents the {status} response" ); } + // The 200 body is documented with the real posture document shape. + assert_eq!( + posture["responses"]["200"]["content"]["application/json"]["example"]["schema"], + json!("registry.ops.posture.v1") + ); } #[test] @@ -4163,6 +4177,7 @@ mod tests { ("/healthz", "get", "200"), ("/ready", "get", "200"), ("/admin/v1/capabilities", "get", "200"), + ("/admin/v1/posture", "get", "200"), ("/admin/v1/config/verify", "post", "200"), ("/admin/v1/config/dry-run", "post", "200"), ("/admin/v1/config/apply", "post", "200"), diff --git a/products/notary/openapi/registry-notary.openapi.json b/products/notary/openapi/registry-notary.openapi.json index dbacf798..f0547f5e 100644 --- a/products/notary/openapi/registry-notary.openapi.json +++ b/products/notary/openapi/registry-notary.openapi.json @@ -2738,6 +2738,103 @@ ], "responses": { "200": { + "content": { + "application/json": { + "example": { + "build": { + "package": "registry-notary", + "version": "0.1.0" + }, + "component": "registry-notary", + "configuration": { + "dynamic_reload_supported": false, + "last_apply_at": null, + "last_apply_result": null, + "last_bundle_id": null, + "last_bundle_sequence": null, + "last_config_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "restart_required": false, + "source": "local_file" + }, + "instance": { + "environment": "production", + "id": "civil-notary-prod", + "jurisdiction": "example-country", + "owner": "Ministry of Interior" + }, + "notary": { + "claim_count": 6, + "credential_profile_count": 2, + "credential_status": { + "enabled": true, + "storage": "redis" + }, + "federation": { + "allowed_profile_count": 3, + "allowed_purpose_count": 4, + "enabled": true, + "peer_count": 2, + "supported_protocol_versions": [ + "registry-notary-federation/v0.1" + ] + }, + "oid4vci": { + "credential_configuration_count": 1, + "enabled": true + }, + "replay": { + "ready": true, + "storage": "redis" + }, + "self_attestation": { + "allowed_claim_count": 1, + "allowed_purpose_count": 1, + "credential_profile_count": 1, + "enabled": true, + "rate_limit_mode": "in_process", + "wallet_origin_count": 1 + }, + "source_connection_counts": { + "http": 2, + "postgres": 1 + } + }, + "observed_at": "2026-06-04T10:00:00Z", + "posture": { + "audit": { + "checkpoint_status": "available", + "configured": true, + "latest_sequence": 88, + "latest_tail_hash": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "sink_type": "file", + "verification_status": "verified", + "verified_at": "2026-06-04T09:59:30Z" + }, + "findings": [], + "warnings": [] + }, + "runtime": { + "admin_enabled": true, + "auth_mode": "oidc", + "readiness": "ready" + }, + "schema": "registry.ops.posture.v1", + "standards_artifacts": { + "evidence_service_discovery": { + "media_type": "application/json", + "observed_status": "available", + "sha256": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "jwks": { + "media_type": "application/jwk-set+json", + "observed_status": "available", + "sha256": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + } + }, + "tier": "default" + } + } + }, "description": "Redacted posture for the requested tier" }, "400": { From 9ce0f257aeabcc380b184c9a373bd0b91dbefc95 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sun, 28 Jun 2026 07:11:50 -0400 Subject: [PATCH 3/3] fix(notary): regenerate OpenAPI on consolidated identifier domain The identifier-domain consolidation (id.registrystack.org) landed on main while this PR was open. Merging it in left the committed OpenAPI artifact stale: the posture and config-apply problem-type examples added here still pointed at docs.registry-notary.dev, which the new identifier_domains test rejects. Regenerate the artifact from the merged generator so the six problem types resolve under https://id.registrystack.org/problems/registry-notary/. Version stays pinned at the published product version 0.6.2 (the generator emits the 0.8.3 crate version). Signed-off-by: Jeremi Joslin --- products/notary/openapi/registry-notary.openapi.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/products/notary/openapi/registry-notary.openapi.json b/products/notary/openapi/registry-notary.openapi.json index 75e22ebd..b9ee7e11 100644 --- a/products/notary/openapi/registry-notary.openapi.json +++ b/products/notary/openapi/registry-notary.openapi.json @@ -2367,7 +2367,7 @@ "detail": "config_trust.antirollback_state_path is not configured", "status": 503, "title": "Config apply unavailable", - "type": "https://docs.registry-notary.dev/problems/config/apply_unavailable" + "type": "https://id.registrystack.org/problems/registry-notary/config/apply_unavailable" }, "schema": { "$ref": "#/components/schemas/ProblemDetails" @@ -2847,7 +2847,7 @@ "schema": "registry.admin.error.v1", "status": 400, "title": "Admin posture tier invalid", - "type": "https://docs.registry-notary.dev/problems/registry/admin/posture/invalid_tier" + "type": "https://id.registrystack.org/problems/registry-notary/registry/admin/posture/invalid_tier" }, "schema": { "$ref": "#/components/schemas/ProblemDetails" @@ -2864,7 +2864,7 @@ "detail": "missing authentication credential", "status": 401, "title": "Missing credential", - "type": "https://docs.registry-notary.dev/problems/auth/missing_credential" + "type": "https://id.registrystack.org/problems/registry-notary/auth/missing_credential" }, "schema": { "$ref": "#/components/schemas/ProblemDetails" @@ -2881,7 +2881,7 @@ "detail": "missing required scope", "status": 403, "title": "Scope denied", - "type": "https://docs.registry-notary.dev/problems/auth/scope_denied" + "type": "https://id.registrystack.org/problems/registry-notary/auth/scope_denied" }, "schema": { "$ref": "#/components/schemas/ProblemDetails" @@ -2898,7 +2898,7 @@ "detail": "admin posture could not be filtered for the requested tier", "status": 500, "title": "Admin posture unavailable", - "type": "https://docs.registry-notary.dev/problems/posture/filter_failed" + "type": "https://id.registrystack.org/problems/registry-notary/posture/filter_failed" }, "schema": { "$ref": "#/components/schemas/ProblemDetails" @@ -2917,7 +2917,7 @@ "schema": "registry.admin.error.v1", "status": 503, "title": "Admin posture unavailable", - "type": "https://docs.registry-notary.dev/problems/posture/unavailable" + "type": "https://id.registrystack.org/problems/registry-notary/posture/unavailable" }, "schema": { "$ref": "#/components/schemas/ProblemDetails"