diff --git a/crates/registry-notary-core/src/model.rs b/crates/registry-notary-core/src/model.rs index 8571f89f..d8aebb5e 100644 --- a/crates/registry-notary-core/src/model.rs +++ b/crates/registry-notary-core/src/model.rs @@ -1593,6 +1593,8 @@ pub struct EvidenceAuditEvent { pub occurred_at: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub principal_id_hash: Option>, + #[serde(default)] + pub scopes_used: Vec, pub decision: String, pub method: String, pub path: String, @@ -2267,6 +2269,7 @@ mod tests { event_id: "01HX".to_string(), occurred_at: "2026-05-25T00:00:00Z".to_string(), principal_id_hash: Some(Hashed::from_hash("hmac-sha256:principal")), + scopes_used: vec!["self_attestation".to_string()], decision: "denied".to_string(), method: "POST".to_string(), path: "/v1/evaluations".to_string(), @@ -2349,6 +2352,7 @@ mod tests { json!("person_is_alive_sd_jwt") ); assert_eq!(value["principal_id_hash"], json!("hmac-sha256:principal")); + assert_eq!(value["scopes_used"], json!(["self_attestation"])); assert_eq!( value["source_sidecar_config_hashes"], json!(["sha256:2222222222222222222222222222222222222222222222222222222222222222"]) @@ -2373,6 +2377,7 @@ mod tests { let decoded: EvidenceAuditEvent = serde_json::from_value(value).expect("audit event deserializes"); assert_eq!(decoded.event_id, event.event_id); + assert_eq!(decoded.scopes_used, vec!["self_attestation"]); assert_eq!(decoded.access_mode, Some(AccessMode::SelfAttestation)); assert_eq!( decoded.denial_code, @@ -2427,6 +2432,7 @@ mod tests { assert!(decoded.verification_id.is_none()); assert!(decoded.claim_hash.is_none()); assert!(decoded.purposes.is_none()); + assert!(decoded.scopes_used.is_empty()); assert!(decoded.row_count.is_none()); assert!(decoded.error_code.is_none()); assert!(decoded.access_mode.is_none()); diff --git a/crates/registry-notary-server/benches/json_bench.rs b/crates/registry-notary-server/benches/json_bench.rs index 86f9b470..bff5bd96 100644 --- a/crates/registry-notary-server/benches/json_bench.rs +++ b/crates/registry-notary-server/benches/json_bench.rs @@ -33,6 +33,7 @@ fn build_audit_event() -> EvidenceAuditEvent { principal_id_hash: Some(Hashed::::from_hash( "hmac-sha256:client-bench-001", )), + scopes_used: vec!["farmer_registry:evidence_verification".to_string()], decision: "allow".to_string(), method: "POST".to_string(), path: "/v1/evaluations".to_string(), diff --git a/crates/registry-notary-server/src/api.rs b/crates/registry-notary-server/src/api.rs index bd48faa5..b5a6fc2a 100644 --- a/crates/registry-notary-server/src/api.rs +++ b/crates/registry-notary-server/src/api.rs @@ -2625,6 +2625,7 @@ fn config_apply_intent_audit_event(audit: ConfigAuditEvent) -> EvidenceAuditEven event_id: Ulid::new().to_string(), occurred_at, principal_id_hash: None, + scopes_used: Vec::new(), decision: "accepted".to_string(), method: "BACKGROUND".to_string(), path: "/__events/admin.config_apply.intent".to_string(), diff --git a/crates/registry-notary-server/src/federation/audit.rs b/crates/registry-notary-server/src/federation/audit.rs index 28554141..b4d82456 100644 --- a/crates/registry-notary-server/src/federation/audit.rs +++ b/crates/registry-notary-server/src/federation/audit.rs @@ -56,6 +56,7 @@ pub(super) fn federation_audit_event( event_id: Ulid::new().to_string(), occurred_at, principal_id_hash: None, + scopes_used: Vec::new(), decision: audit.decision, method: "POST".to_string(), path: "/federation/v1/evaluations".to_string(), diff --git a/crates/registry-notary-server/src/standalone.rs b/crates/registry-notary-server/src/standalone.rs index eed5bd0f..011c3240 100644 --- a/crates/registry-notary-server/src/standalone.rs +++ b/crates/registry-notary-server/src/standalone.rs @@ -3291,6 +3291,7 @@ pub(crate) fn pre_auth_audit_event( event_id: Ulid::new().to_string(), occurred_at, principal_id_hash: fields.principal_id_hash, + scopes_used: Vec::new(), decision: decision.to_string(), method: method.to_string(), path: path.to_string(), @@ -4629,6 +4630,7 @@ fn build_audit_event( principal_id_hash: principal.map(|principal| { Hashed::::from_hash(hasher.hash(&principal.principal_id)) }), + scopes_used: principal.map_or_else(Vec::new, |principal| principal.scopes.clone()), decision, method: method.to_string(), path: path.to_string(), @@ -7993,6 +7995,7 @@ credential_profiles: event_id: "01HX0000000000000000000000".to_string(), occurred_at: "2026-05-22T00:00:00Z".to_string(), principal_id_hash: Some(Hashed::from_hash("sha256:caseworker")), + scopes_used: vec!["registry_notary:admin".to_string()], decision: "allowed".to_string(), method: "GET".to_string(), path: "/v1/claims".to_string(), diff --git a/crates/registry-notary-server/tests/standalone_http.rs b/crates/registry-notary-server/tests/standalone_http.rs index 675e810c..b3f17027 100644 --- a/crates/registry-notary-server/tests/standalone_http.rs +++ b/crates/registry-notary-server/tests/standalone_http.rs @@ -8195,6 +8195,15 @@ async fn oidc_mode_verifies_token_from_fixture_idp() { assert!(envelopes .iter() .all(|envelope| envelope.record.get("principal_id").is_none())); + let claims_audit = envelopes + .iter() + .map(|envelope| &envelope.record) + .find(|record| record["path"] == json!("/v1/claims") && record["status"] == json!(200)) + .expect("claims audit record exists"); + assert_eq!( + claims_audit["scopes_used"], + json!(["farmer_registry:evidence_verification"]) + ); assert!(!audit.contains(&token)); idp.stop().await; @@ -8427,6 +8436,7 @@ async fn oidc_self_attestation_evaluates_renders_and_audits_access_mode() { .starts_with("hmac-sha256:")); assert!(evaluate_audit.get("principal_id").is_none()); assert!(evaluate_audit.get("principal_id_hash").is_some()); + assert_eq!(evaluate_audit["scopes_used"], json!(["self_attestation"])); let render_audit = records .iter() @@ -8437,6 +8447,7 @@ async fn oidc_self_attestation_evaluates_renders_and_audits_access_mode() { }) .expect("render audit record exists"); assert_eq!(render_audit["access_mode"], json!("self_attestation")); + assert_eq!(render_audit["scopes_used"], json!(["self_attestation"])); assert_eq!( render_audit["purposes"], json!(["citizen_self_attestation"])