From 4ffa6d10cfcd1f54cd59035bb9d441f77f154678 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Thu, 22 Aug 2024 10:28:16 +0200 Subject: [PATCH 01/31] feat init agetn_holder --- Cargo.lock | 40 +- Cargo.toml | 11 +- agent_holder/Cargo.toml | 45 ++ agent_holder/src/credential/README.md | 6 + agent_holder/src/credential/aggregate.rs | 306 +++++++++++ agent_holder/src/credential/command.rs | 20 + agent_holder/src/credential/entity.rs | 6 + agent_holder/src/credential/error.rs | 22 + agent_holder/src/credential/event.rs | 26 + agent_holder/src/credential/mod.rs | 6 + agent_holder/src/credential/queries.rs | 30 + agent_holder/src/lib.rs | 3 + agent_holder/src/offer/README.md | 10 + agent_holder/src/offer/aggregate.rs | 515 ++++++++++++++++++ agent_holder/src/offer/command.rs | 23 + agent_holder/src/offer/error.rs | 13 + agent_holder/src/offer/event.rs | 50 ++ agent_holder/src/offer/mod.rs | 5 + .../src/offer/queries/access_token.rs | 87 +++ agent_holder/src/offer/queries/mod.rs | 67 +++ .../src/offer/queries/pre_authorized_code.rs | 90 +++ agent_holder/src/services.rs | 64 +++ 22 files changed, 1434 insertions(+), 11 deletions(-) create mode 100644 agent_holder/Cargo.toml create mode 100644 agent_holder/src/credential/README.md create mode 100644 agent_holder/src/credential/aggregate.rs create mode 100644 agent_holder/src/credential/command.rs create mode 100644 agent_holder/src/credential/entity.rs create mode 100644 agent_holder/src/credential/error.rs create mode 100644 agent_holder/src/credential/event.rs create mode 100644 agent_holder/src/credential/mod.rs create mode 100644 agent_holder/src/credential/queries.rs create mode 100644 agent_holder/src/lib.rs create mode 100644 agent_holder/src/offer/README.md create mode 100644 agent_holder/src/offer/aggregate.rs create mode 100644 agent_holder/src/offer/command.rs create mode 100644 agent_holder/src/offer/error.rs create mode 100644 agent_holder/src/offer/event.rs create mode 100644 agent_holder/src/offer/mod.rs create mode 100644 agent_holder/src/offer/queries/access_token.rs create mode 100644 agent_holder/src/offer/queries/mod.rs create mode 100644 agent_holder/src/offer/queries/pre_authorized_code.rs create mode 100644 agent_holder/src/services.rs diff --git a/Cargo.lock b/Cargo.lock index 1f42cbae..7aaedc6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -144,6 +144,34 @@ dependencies = [ "wiremock", ] +[[package]] +name = "agent_holder" +version = "0.1.0" +dependencies = [ + "agent_secret_manager", + "agent_shared", + "async-trait", + "axum 0.7.5", + "chrono", + "cqrs-es", + "derivative", + "futures", + "identity_core", + "identity_credential", + "jsonschema", + "jsonwebtoken", + "oid4vc-core", + "oid4vc-manager", + "oid4vci", + "serde", + "serde_json", + "thiserror", + "tracing", + "types-ob-v3", + "url", + "uuid", +] + [[package]] name = "agent_issuance" version = "0.1.0" @@ -2029,7 +2057,7 @@ dependencies = [ [[package]] name = "dif-presentation-exchange" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=12fed14#12fed1411ff3c0e1797090f386e44694f7a279b8" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=6c1cdb6#6c1cdb66b93c091725ca8709ee33e03f28ad65a2" dependencies = [ "getset", "jsonpath_lib", @@ -4676,7 +4704,7 @@ dependencies = [ [[package]] name = "oid4vc-core" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=12fed14#12fed1411ff3c0e1797090f386e44694f7a279b8" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=6c1cdb6#6c1cdb66b93c091725ca8709ee33e03f28ad65a2" dependencies = [ "anyhow", "async-trait", @@ -4700,7 +4728,7 @@ dependencies = [ [[package]] name = "oid4vc-manager" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=12fed14#12fed1411ff3c0e1797090f386e44694f7a279b8" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=6c1cdb6#6c1cdb66b93c091725ca8709ee33e03f28ad65a2" dependencies = [ "anyhow", "async-trait", @@ -4732,7 +4760,7 @@ dependencies = [ [[package]] name = "oid4vci" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=12fed14#12fed1411ff3c0e1797090f386e44694f7a279b8" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=6c1cdb6#6c1cdb66b93c091725ca8709ee33e03f28ad65a2" dependencies = [ "anyhow", "derivative", @@ -4755,7 +4783,7 @@ dependencies = [ [[package]] name = "oid4vp" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=12fed14#12fed1411ff3c0e1797090f386e44694f7a279b8" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=6c1cdb6#6c1cdb66b93c091725ca8709ee33e03f28ad65a2" dependencies = [ "anyhow", "chrono", @@ -6617,7 +6645,7 @@ dependencies = [ [[package]] name = "siopv2" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=12fed14#12fed1411ff3c0e1797090f386e44694f7a279b8" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=6c1cdb6#6c1cdb66b93c091725ca8709ee33e03f28ad65a2" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 6c185cbd..186bf2d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "agent_api_rest", "agent_application", "agent_event_publisher_http", + "agent_holder", "agent_issuance", "agent_secret_manager", "agent_shared", @@ -18,11 +19,11 @@ rust-version = "1.76.0" [workspace.dependencies] did_manager = { git = "https://git@github.com/impierce/did-manager.git", rev = "2bda2b8" } -siopv2 = { git = "https://git@github.com/impierce/openid4vc.git", rev = "12fed14" } -oid4vci = { git = "https://git@github.com/impierce/openid4vc.git", rev = "12fed14" } -oid4vc-core = { git = "https://git@github.com/impierce/openid4vc.git", rev = "12fed14" } -oid4vc-manager = { git = "https://git@github.com/impierce/openid4vc.git", rev = "12fed14" } -oid4vp = { git = "https://git@github.com/impierce/openid4vc.git", rev = "12fed14" } +siopv2 = { git = "https://git@github.com/impierce/openid4vc.git", rev = "6c1cdb6" } +oid4vci = { git = "https://git@github.com/impierce/openid4vc.git", rev = "6c1cdb6" } +oid4vc-core = { git = "https://git@github.com/impierce/openid4vc.git", rev = "6c1cdb6" } +oid4vc-manager = { git = "https://git@github.com/impierce/openid4vc.git", rev = "6c1cdb6" } +oid4vp = { git = "https://git@github.com/impierce/openid4vc.git", rev = "6c1cdb6" } async-trait = "0.1" axum = { version = "0.7", features = ["tracing"] } diff --git a/agent_holder/Cargo.toml b/agent_holder/Cargo.toml new file mode 100644 index 00000000..d983de73 --- /dev/null +++ b/agent_holder/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "agent_holder" +version.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +agent_shared = { path = "../agent_shared" } +agent_secret_manager = { path = "../agent_secret_manager" } + +async-trait.workspace = true +axum.workspace = true +cqrs-es.workspace = true +chrono = "0.4" +types-ob-v3 = { git = "https://github.com/impierce/digital-credential-data-models.git", rev = "9f16c27" } +derivative = "2.2" +futures.workspace = true +identity_core = "1.3" +identity_credential.workspace = true +jsonschema = "0.17" +jsonwebtoken.workspace = true +oid4vci.workspace = true +oid4vc-core.workspace = true +oid4vc-manager.workspace = true +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true +tracing.workspace = true +url.workspace = true +uuid.workspace = true + +# [dev-dependencies] +# agent_issuance = { path = ".", features = ["test_utils"] } +# agent_shared = { path = "../agent_shared", features = ["test_utils"] } + +# did_manager.workspace = true +# lazy_static.workspace = true +# serial_test = "3.0" +# tokio.workspace = true +# tracing-test.workspace = true +# async-std = { version = "1.5", features = ["attributes", "tokio1"] } +# rstest.workspace = true + +[features] +test_utils = [] diff --git a/agent_holder/src/credential/README.md b/agent_holder/src/credential/README.md new file mode 100644 index 00000000..ce77f83b --- /dev/null +++ b/agent_holder/src/credential/README.md @@ -0,0 +1,6 @@ +# Credential + +This aggregate is defined by: + +- credential data +- a format (such as: _Open Badge 3.0_) diff --git a/agent_holder/src/credential/aggregate.rs b/agent_holder/src/credential/aggregate.rs new file mode 100644 index 00000000..9c65c71b --- /dev/null +++ b/agent_holder/src/credential/aggregate.rs @@ -0,0 +1,306 @@ +use agent_shared::config::{config, get_preferred_did_method, get_preferred_signing_algorithm}; +use async_trait::async_trait; +use cqrs_es::Aggregate; +use derivative::Derivative; +use identity_core::convert::FromJson; +use identity_credential::credential::{ + Credential as W3CVerifiableCredential, CredentialBuilder as W3CVerifiableCredentialBuilder, Issuer, +}; +use jsonwebtoken::Header; +use oid4vc_core::jwt; +use oid4vci::credential_format_profiles::w3c_verifiable_credentials::jwt_vc_json::{ + CredentialDefinition, JwtVcJson, JwtVcJsonParameters, +}; +use oid4vci::credential_format_profiles::{CredentialFormats, Parameters}; +use oid4vci::credential_issuer::credential_configurations_supported::CredentialConfigurationsSupportedObject; +use oid4vci::credential_response::CredentialResponseType; +use oid4vci::VerifiableCredentialJwt; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::sync::Arc; +use tracing::info; +use types_ob_v3::prelude::{ + AchievementCredential, AchievementCredentialBuilder, AchievementCredentialType, AchievementSubject, Profile, + ProfileBuilder, +}; + +use crate::credential::command::CredentialCommand; +use crate::credential::error::CredentialError::{self}; +use crate::credential::event::CredentialEvent; +use crate::services::HolderServices; + +use super::entity::Data; + +#[derive(Debug, Clone, Serialize, Deserialize, Default, Derivative)] +#[derivative(PartialEq)] +pub struct Credential { + pub credential_id: Option, + pub offer_id: Option, + pub credential: Option, +} + +#[async_trait] +impl Aggregate for Credential { + type Command = CredentialCommand; + type Event = CredentialEvent; + type Error = CredentialError; + type Services = Arc; + + fn aggregate_type() -> String { + "credential".to_string() + } + + async fn handle(&self, command: Self::Command, services: &Self::Services) -> Result, Self::Error> { + use CredentialCommand::*; + use CredentialError::*; + use CredentialEvent::*; + + info!("Handling command: {:?}", command); + + match command { + AddCredential { + credential_id, + offer_id, + credential, + } => Ok(vec![CredentialAdded { + credential_id, + offer_id, + credential, + }]), + } + } + + fn apply(&mut self, event: Self::Event) { + use CredentialEvent::*; + + info!("Applying event: {:?}", event); + + match event { + CredentialAdded { + credential_id, + offer_id, + credential, + } => { + self.credential_id = Some(credential_id); + self.offer_id = Some(offer_id); + self.credential = Some(credential); + } + } + } +} + +// #[cfg(test)] +// pub mod credential_tests { +// use std::collections::HashMap; + +// use super::*; + +// use jsonwebtoken::Algorithm; +// use lazy_static::lazy_static; +// use oid4vci::proof::KeyProofMetadata; +// use oid4vci::ProofType; +// use rstest::rstest; +// use serde_json::json; + +// use cqrs_es::test::TestFramework; + +// use crate::credential::aggregate::Credential; +// use crate::credential::event::CredentialEvent; +// use crate::offer::aggregate::tests::SUBJECT_KEY_DID; +// use crate::services::test_utils::test_issuance_services; + +// type CredentialTestFramework = TestFramework; + +// #[rstest] +// #[case::openbadges( +// OPENBADGE_CREDENTIAL_SUBJECT.clone(), +// OPENBADGE_CREDENTIAL_CONFIGURATION.clone(), +// UNSIGNED_OPENBADGE_CREDENTIAL.clone() +// )] +// #[case::w3c_vc( +// W3C_VC_CREDENTIAL_SUBJECT.clone(), +// W3C_VC_CREDENTIAL_CONFIGURATION.clone(), +// UNSIGNED_W3C_VC_CREDENTIAL.clone() +// )] +// #[serial_test::serial] +// fn test_create_unsigned_credential( +// #[case] credential_subject: serde_json::Value, +// #[case] credential_configuration: CredentialConfigurationsSupportedObject, +// #[case] unsigned_credential: serde_json::Value, +// ) { +// CredentialTestFramework::with(test_issuance_services()) +// .given_no_previous_events() +// .when(CredentialCommand::CreateUnsignedCredential { +// data: Data { +// raw: credential_subject, +// }, +// credential_configuration: credential_configuration.clone(), +// }) +// .then_expect_events(vec![CredentialEvent::UnsignedCredentialCreated { +// data: Data { +// raw: unsigned_credential, +// }, +// credential_configuration, +// }]) +// } + +// #[rstest] +// #[case::openbadges( +// UNSIGNED_OPENBADGE_CREDENTIAL.clone(), +// OPENBADGE_CREDENTIAL_CONFIGURATION.clone(), +// OPENBADGE_VERIFIABLE_CREDENTIAL_JWT.to_string(), +// )] +// #[case::w3c_vc( +// UNSIGNED_W3C_VC_CREDENTIAL.clone(), +// W3C_VC_CREDENTIAL_CONFIGURATION.clone(), +// W3C_VC_VERIFIABLE_CREDENTIAL_JWT.to_string(), +// )] +// #[serial_test::serial] +// async fn test_sign_credential( +// #[case] unsigned_credential: serde_json::Value, +// #[case] credential_configuration: CredentialConfigurationsSupportedObject, +// #[case] verifiable_credential_jwt: String, +// ) { +// CredentialTestFramework::with(test_issuance_services()) +// .given(vec![CredentialEvent::UnsignedCredentialCreated { +// data: Data { +// raw: unsigned_credential, +// }, +// credential_configuration, +// }]) +// .when(CredentialCommand::SignCredential { +// subject_id: SUBJECT_KEY_DID.identifier("did:key", Algorithm::EdDSA).await.unwrap(), +// overwrite: false, +// }) +// .then_expect_events(vec![CredentialEvent::CredentialSigned { +// signed_credential: json!(verifiable_credential_jwt), +// }]) +// } + +// lazy_static! { +// static ref OPENBADGE_CREDENTIAL_CONFIGURATION: CredentialConfigurationsSupportedObject = +// CredentialConfigurationsSupportedObject { +// credential_format: CredentialFormats::JwtVcJson(Parameters { +// parameters: ( +// CredentialDefinition { +// type_: vec!["VerifiableCredential".to_string(), "OpenBadgeCredential".to_string()], +// credential_subject: Default::default(), +// }, +// None, +// ) +// .into(), +// }), +// cryptographic_binding_methods_supported: vec![ +// "did:key".to_string(), +// "did:key".to_string(), +// "did:iota:rms".to_string(), +// "did:jwk".to_string(), +// ], +// credential_signing_alg_values_supported: vec!["EdDSA".to_string()], +// proof_types_supported: HashMap::from_iter(vec![( +// ProofType::Jwt, +// KeyProofMetadata { +// proof_signing_alg_values_supported: vec![Algorithm::EdDSA], +// }, +// )]), +// display: vec![json!({ +// "name": "Teamwork Badge", +// "logo": { +// "url": "https://example.com/logo.png" +// } +// })], +// ..Default::default() +// }; +// static ref W3C_VC_CREDENTIAL_CONFIGURATION: CredentialConfigurationsSupportedObject = +// CredentialConfigurationsSupportedObject { +// credential_format: CredentialFormats::JwtVcJson(Parameters { +// parameters: ( +// CredentialDefinition { +// type_: vec!["VerifiableCredential".to_string()], +// credential_subject: Default::default(), +// }, +// None, +// ) +// .into(), +// }), +// cryptographic_binding_methods_supported: vec![ +// "did:key".to_string(), +// "did:key".to_string(), +// "did:iota:rms".to_string(), +// "did:jwk".to_string(), +// ], +// credential_signing_alg_values_supported: vec!["EdDSA".to_string()], +// proof_types_supported: HashMap::from_iter(vec![( +// ProofType::Jwt, +// KeyProofMetadata { +// proof_signing_alg_values_supported: vec![Algorithm::EdDSA], +// }, +// )]), +// display: vec![json!({ +// "name": "Master Degree", +// "logo": { +// "url": "https://example.com/logo.png" +// } +// })], +// ..Default::default() +// }; +// static ref OPENBADGE_CREDENTIAL_SUBJECT: serde_json::Value = json!( +// { +// "credentialSubject": { +// "type": [ "AchievementSubject" ], +// "achievement": { +// "id": "https://example.com/achievements/21st-century-skills/teamwork", +// "type": "Achievement", +// "criteria": { +// "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." +// }, +// "description": "This badge recognizes the development of the capacity to collaborate within a group environment.", +// "name": "Teamwork" +// } +// } +// } +// ); +// static ref W3C_VC_CREDENTIAL_SUBJECT: serde_json::Value = json!( +// { +// "credentialSubject": { +// "first_name": "Ferris", +// "last_name": "Rustacean", +// "degree": { +// "type": "MasterDegree", +// "name": "Master of Oceanography" +// } +// } +// } +// ); +// static ref UNSIGNED_OPENBADGE_CREDENTIAL: serde_json::Value = json!({ +// "@context": [ +// "https://www.w3.org/2018/credentials/v1", +// "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json" +// ], +// "id": "http://example.com/credentials/3527", +// "type": ["VerifiableCredential", "OpenBadgeCredential"], +// "issuer": { +// "id": "https://my-domain.example.org", +// "type": "Profile", +// "name": "UniCore" +// }, +// "issuanceDate": "2010-01-01T00:00:00Z", +// "name": "Teamwork Badge", +// "credentialSubject": OPENBADGE_CREDENTIAL_SUBJECT["credentialSubject"].clone(), +// }); +// static ref UNSIGNED_W3C_VC_CREDENTIAL: serde_json::Value = json!({ +// "@context": "https://www.w3.org/2018/credentials/v1", +// "type": [ "VerifiableCredential" ], +// "credentialSubject": W3C_VC_CREDENTIAL_SUBJECT["credentialSubject"].clone(), +// "issuer": { +// "id": "https://my-domain.example.org/", +// "name": "UniCore" +// }, +// "issuanceDate": "2010-01-01T00:00:00Z" +// }); +// } + +// pub const OPENBADGE_VERIFIABLE_CREDENTIAL_JWT: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjIuanNvbiJdLCJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9jcmVkZW50aWFscy8zNTI3IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIk9wZW5CYWRnZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiIsIm5hbWUiOiJUZWFtd29yayBCYWRnZSIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJ0eXBlIjpbIkFjaGlldmVtZW50U3ViamVjdCJdLCJhY2hpZXZlbWVudCI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vYWNoaWV2ZW1lbnRzLzIxc3QtY2VudHVyeS1za2lsbHMvdGVhbXdvcmsiLCJ0eXBlIjoiQWNoaWV2ZW1lbnQiLCJjcml0ZXJpYSI6eyJuYXJyYXRpdmUiOiJUZWFtIG1lbWJlcnMgYXJlIG5vbWluYXRlZCBmb3IgdGhpcyBiYWRnZSBieSB0aGVpciBwZWVycyBhbmQgcmVjb2duaXplZCB1cG9uIHJldmlldyBieSBFeGFtcGxlIENvcnAgbWFuYWdlbWVudC4ifSwiZGVzY3JpcHRpb24iOiJUaGlzIGJhZGdlIHJlY29nbml6ZXMgdGhlIGRldmVsb3BtZW50IG9mIHRoZSBjYXBhY2l0eSB0byBjb2xsYWJvcmF0ZSB3aXRoaW4gYSBncm91cCBlbnZpcm9ubWVudC4iLCJuYW1lIjoiVGVhbXdvcmsifX19fQ.SkC7IvpBGB9e98eobnE9qcLjs-yoZup3cieBla3DRTlcRezXEDPv4YRoUgffho9LJ0rkmfFPsPwb-owXMWyPAA"; + +// pub const W3C_VC_VERIFIABLE_CREDENTIAL_JWT: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJmaXJzdF9uYW1lIjoiRmVycmlzIiwibGFzdF9uYW1lIjoiUnVzdGFjZWFuIiwiZGVncmVlIjp7InR5cGUiOiJNYXN0ZXJEZWdyZWUiLCJuYW1lIjoiTWFzdGVyIG9mIE9jZWFub2dyYXBoeSJ9fSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiJ9fQ.MUDBbPJfXe0G9sjVTF3RuR6ukRM0d4N57iMGNFcIKMFPIEdig12v-YFB0qfnSghGcQo8hUw3jzxZXTSJATEgBg"; +// } diff --git a/agent_holder/src/credential/command.rs b/agent_holder/src/credential/command.rs new file mode 100644 index 00000000..7385f003 --- /dev/null +++ b/agent_holder/src/credential/command.rs @@ -0,0 +1,20 @@ +use oid4vci::{ + credential_issuer::{ + credential_configurations_supported::CredentialConfigurationsSupportedObject, + credential_issuer_metadata::CredentialIssuerMetadata, + }, + token_response::TokenResponse, +}; +use serde::Deserialize; + +use super::entity::Data; + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum CredentialCommand { + AddCredential { + credential_id: String, + offer_id: String, + credential: serde_json::Value, + }, +} diff --git a/agent_holder/src/credential/entity.rs b/agent_holder/src/credential/entity.rs new file mode 100644 index 00000000..432325fb --- /dev/null +++ b/agent_holder/src/credential/entity.rs @@ -0,0 +1,6 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct Data { + pub raw: serde_json::Value, +} diff --git a/agent_holder/src/credential/error.rs b/agent_holder/src/credential/error.rs new file mode 100644 index 00000000..c03f6492 --- /dev/null +++ b/agent_holder/src/credential/error.rs @@ -0,0 +1,22 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum CredentialError { + #[error("Credential must be an object")] + InvalidCredentialError, + + #[error("This Credential format it not supported")] + UnsupportedCredentialFormat, + + #[error("The `credentialSubject` parameter is missing")] + MissingCredentialSubjectError, + + #[error("The supplied `credentialSubject` is invalid: {0}")] + InvalidCredentialSubjectError(String), + + #[error("The verifiable credential is invalid: {0}")] + InvalidVerifiableCredentialError(String), + + #[error("Could not find any data to be signed")] + MissingCredentialDataError, +} diff --git a/agent_holder/src/credential/event.rs b/agent_holder/src/credential/event.rs new file mode 100644 index 00000000..cbc50106 --- /dev/null +++ b/agent_holder/src/credential/event.rs @@ -0,0 +1,26 @@ +use cqrs_es::DomainEvent; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum CredentialEvent { + CredentialAdded { + credential_id: String, + offer_id: String, + credential: serde_json::Value, + }, +} + +impl DomainEvent for CredentialEvent { + fn event_type(&self) -> String { + use CredentialEvent::*; + + let event_type: &str = match self { + CredentialAdded { .. } => "CredentialAdded", + }; + event_type.to_string() + } + + fn event_version(&self) -> String { + "1".to_string() + } +} diff --git a/agent_holder/src/credential/mod.rs b/agent_holder/src/credential/mod.rs new file mode 100644 index 00000000..5c6981d1 --- /dev/null +++ b/agent_holder/src/credential/mod.rs @@ -0,0 +1,6 @@ +pub mod aggregate; +pub mod command; +pub mod entity; +pub mod error; +pub mod event; +pub mod queries; diff --git a/agent_holder/src/credential/queries.rs b/agent_holder/src/credential/queries.rs new file mode 100644 index 00000000..164263a9 --- /dev/null +++ b/agent_holder/src/credential/queries.rs @@ -0,0 +1,30 @@ +use super::{entity::Data, event::CredentialEvent}; +use crate::credential::aggregate::Credential; +use cqrs_es::{EventEnvelope, View}; +use oid4vci::credential_issuer::credential_configurations_supported::CredentialConfigurationsSupportedObject; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct CredentialView { + pub credential_id: Option, + pub offer_id: Option, + pub credential: Option, +} + +impl View for CredentialView { + fn update(&mut self, event: &EventEnvelope) { + use CredentialEvent::*; + + match &event.payload { + CredentialAdded { + credential_id, + offer_id, + credential, + } => { + self.credential_id.replace(credential_id.clone()); + self.offer_id.replace(offer_id.clone()); + self.credential.replace(credential.clone()); + } + } + } +} diff --git a/agent_holder/src/lib.rs b/agent_holder/src/lib.rs new file mode 100644 index 00000000..185af944 --- /dev/null +++ b/agent_holder/src/lib.rs @@ -0,0 +1,3 @@ +pub mod credential; +pub mod offer; +pub mod services; diff --git a/agent_holder/src/offer/README.md b/agent_holder/src/offer/README.md new file mode 100644 index 00000000..4c0e60ac --- /dev/null +++ b/agent_holder/src/offer/README.md @@ -0,0 +1,10 @@ +# Offer + +This aggregate holds everything related to an offer of a credential to a subject: + +- credential_ids +- form_url_encoded_credential_offer +- pre_authorized_code +- token_response +- access_token +- credential_response diff --git a/agent_holder/src/offer/aggregate.rs b/agent_holder/src/offer/aggregate.rs new file mode 100644 index 00000000..c9d291de --- /dev/null +++ b/agent_holder/src/offer/aggregate.rs @@ -0,0 +1,515 @@ +use agent_shared::generate_random_string; +use async_trait::async_trait; +use cqrs_es::Aggregate; +use oid4vc_core::Validator; +use oid4vci::credential_issuer::credential_configurations_supported::CredentialConfigurationsSupportedObject; +use oid4vci::credential_issuer::CredentialIssuer; +use oid4vci::credential_offer::{CredentialOffer, CredentialOfferParameters, Grants, PreAuthorizedCode}; +use oid4vci::credential_request::CredentialRequest; +use oid4vci::credential_response::{CredentialResponse, CredentialResponseType}; +use oid4vci::token_request::TokenRequest; +use oid4vci::token_response::TokenResponse; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; +use tracing::info; + +use crate::offer::command::OfferCommand; +use crate::offer::error::OfferError::{self, *}; +use crate::offer::event::OfferEvent; +use crate::services::HolderServices; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Offer { + pub credential_offer: Option, + pub credential_configurations: Option>, + pub token_response: Option, + pub credentials: Vec, + // pub subject_id: Option, + // pub credential_ids: Vec, + // pub form_url_encoded_credential_offer: String, + // pub pre_authorized_code: String, + // pub token_response: Option, + // pub access_token: String, + // pub credential_response: Option, +} + +#[async_trait] +impl Aggregate for Offer { + type Command = OfferCommand; + type Event = OfferEvent; + type Error = OfferError; + type Services = Arc; + + fn aggregate_type() -> String { + "offer".to_string() + } + + async fn handle(&self, command: Self::Command, services: &Self::Services) -> Result, Self::Error> { + use OfferCommand::*; + use OfferEvent::*; + + info!("Handling command: {:?}", command); + + match command { + ReceiveCredentialOffer { + offer_id, + credential_offer, + } => { + let wallet = &services.wallet; + + let credential_offer = match credential_offer { + CredentialOffer::CredentialOfferUri(credential_offer_uri) => services + .wallet + .get_credential_offer(credential_offer_uri) + .await + .unwrap(), + CredentialOffer::CredentialOffer(credential_offer) => *credential_offer, + }; + + // The credential offer contains a credential issuer url. + let credential_issuer_url = credential_offer.credential_issuer.clone(); + + // Get the credential issuer metadata. + let credential_issuer_metadata = wallet + .get_credential_issuer_metadata(credential_issuer_url.clone()) + .await + .unwrap(); + + let credential_configurations: HashMap = + credential_issuer_metadata + .credential_configurations_supported + .iter() + .filter(|(id, _)| credential_offer.credential_configuration_ids.contains(id)) + .map(|(id, credential_configuration)| (id.clone(), credential_configuration.clone())) + .collect(); + + Ok(vec![CredentialOfferReceived { + offer_id, + credential_offer, + credential_configurations, + }]) + } + AcceptCredentialOffer { offer_id } => Ok(vec![CredentialOfferAccepted { offer_id }]), + SendTokenRequest { offer_id } => { + let wallet = &services.wallet; + + let credential_issuer_url = self.credential_offer.as_ref().unwrap().credential_issuer.clone(); + + // Get the authorization server metadata. + let authorization_server_metadata = wallet + .get_authorization_server_metadata(credential_issuer_url.clone()) + .await + .unwrap(); + + // Create a token request with grant_type `pre_authorized_code`. + let token_request = match self.credential_offer.as_ref().unwrap().grants.clone() { + Some(Grants { + pre_authorized_code, .. + }) => TokenRequest::PreAuthorizedCode { + pre_authorized_code: pre_authorized_code.unwrap().pre_authorized_code, + tx_code: None, + }, + None => unreachable!(), + }; + + info!("token_request: {:?}", token_request); + + // Get an access token. + let token_response = wallet + .get_access_token(authorization_server_metadata.token_endpoint.unwrap(), token_request) + .await + .unwrap(); + + info!("token_response: {:?}", token_response); + + Ok(vec![TokenResponseReceived { + offer_id, + token_response, + }]) + } + SendCredentialRequest { offer_id } => { + let wallet = &services.wallet; + + let credential_issuer_url = self.credential_offer.as_ref().unwrap().credential_issuer.clone(); + + // Get an access token. + let token_response = self.token_response.as_ref().unwrap().clone(); + + let credential_configuration_ids = self + .credential_offer + .as_ref() + .unwrap() + .credential_configuration_ids + .clone(); + + // Get the credential issuer metadata. + let credential_issuer_metadata = wallet + .get_credential_issuer_metadata(credential_issuer_url.clone()) + .await + .unwrap(); + + let credentials: Vec = match credential_configuration_ids.len() { + 0 => vec![], + 1 => { + let credential_configuration_id = credential_configuration_ids[0].clone(); + + let credential_configuration = self + .credential_configurations + .as_ref() + .unwrap() + .get(&credential_configuration_id) + .unwrap(); + + // Get the credential. + let credential_response = wallet + .get_credential(credential_issuer_metadata, &token_response, credential_configuration) + .await + .unwrap(); + + let credential = match credential_response.credential { + CredentialResponseType::Immediate { credential, .. } => credential, + _ => panic!("Credential was not a jwt_vc_json."), + }; + + vec![credential] + } + _batch => { + todo!() + } + }; + + info!("credentials: {:?}", credentials); + + Ok(vec![CredentialResponseReceived { offer_id, credentials }]) + } + RejectCredentialOffer { offer_id } => todo!(), + } + } + + fn apply(&mut self, event: Self::Event) { + use OfferEvent::*; + + info!("Applying event: {:?}", event); + + match event { + CredentialOfferReceived { credential_offer, .. } => { + self.credential_offer.replace(credential_offer); + } + TokenResponseReceived { token_response, .. } => { + self.token_response.replace(token_response); + } + CredentialResponseReceived { credentials, .. } => { + self.credentials = credentials; + } + CredentialOfferAccepted { .. } => {} + CredentialOfferRejected { .. } => {} + } + } +} + +// #[cfg(test)] +// pub mod tests { +// use super::*; + +// use cqrs_es::test::TestFramework; +// use jsonwebtoken::Algorithm; +// use lazy_static::lazy_static; +// use oid4vci::{ +// credential_format_profiles::{ +// w3c_verifiable_credentials::jwt_vc_json::CredentialDefinition, CredentialFormats, Parameters, +// }, +// credential_request::CredentialRequest, +// KeyProofType, ProofType, +// }; +// use rstest::rstest; +// use serde_json::json; +// use std::{collections::VecDeque, sync::Mutex}; + +// use crate::{ +// credential::aggregate::credential_tests::OPENBADGE_VERIFIABLE_CREDENTIAL_JWT, +// server_config::aggregate::server_config_tests::{AUTHORIZATION_SERVER_METADATA, CREDENTIAL_ISSUER_METADATA}, +// services::test_utils::test_issuance_services, +// }; + +// type OfferTestFramework = TestFramework; + +// #[test] +// #[serial_test::serial] +// fn test_create_offer() { +// *PRE_AUTHORIZED_CODES.lock().unwrap() = vec![generate_random_string()].into(); +// *ACCESS_TOKENS.lock().unwrap() = vec![generate_random_string()].into(); +// *C_NONCES.lock().unwrap() = vec![generate_random_string()].into(); + +// let subject = test_subject(); +// OfferTestFramework::with(test_issuance_services()) +// .given_no_previous_events() +// .when(OfferCommand::CreateCredentialOffer { +// offer_id: Default::default(), +// }) +// .then_expect_events(vec![OfferEvent::CredentialOfferCreated { +// offer_id: Default::default(), +// pre_authorized_code: subject.pre_authorized_code, +// access_token: subject.access_token, +// }]); +// } + +// #[test] +// #[serial_test::serial] +// fn test_add_credential() { +// *PRE_AUTHORIZED_CODES.lock().unwrap() = vec![generate_random_string()].into(); +// *ACCESS_TOKENS.lock().unwrap() = vec![generate_random_string()].into(); +// *C_NONCES.lock().unwrap() = vec![generate_random_string()].into(); + +// let subject = test_subject(); +// OfferTestFramework::with(test_issuance_services()) +// .given(vec![OfferEvent::CredentialOfferCreated { +// offer_id: Default::default(), +// pre_authorized_code: subject.pre_authorized_code.clone(), +// access_token: subject.access_token.clone(), +// }]) +// .when(OfferCommand::AddCredentials { +// offer_id: Default::default(), +// credential_ids: vec!["credential-id".to_string()], +// }) +// .then_expect_events(vec![OfferEvent::CredentialsAdded { +// offer_id: Default::default(), +// credential_ids: vec!["credential-id".to_string()], +// }]); +// } + +// #[test] +// #[serial_test::serial] +// fn test_create_credential_offer() { +// *PRE_AUTHORIZED_CODES.lock().unwrap() = vec![generate_random_string()].into(); +// *ACCESS_TOKENS.lock().unwrap() = vec![generate_random_string()].into(); +// *C_NONCES.lock().unwrap() = vec![generate_random_string()].into(); + +// let subject = test_subject(); +// OfferTestFramework::with(test_issuance_services()) +// .given(vec![ +// OfferEvent::CredentialOfferCreated { +// offer_id: Default::default(), +// pre_authorized_code: subject.pre_authorized_code, +// access_token: subject.access_token, +// }, +// OfferEvent::CredentialsAdded { +// offer_id: Default::default(), +// credential_ids: vec!["credential-id".to_string()], +// }, +// ]) +// .when(OfferCommand::CreateFormUrlEncodedCredentialOffer { +// offer_id: Default::default(), +// credential_issuer_metadata: CREDENTIAL_ISSUER_METADATA.clone(), +// }) +// .then_expect_events(vec![OfferEvent::FormUrlEncodedCredentialOfferCreated { +// offer_id: Default::default(), +// form_url_encoded_credential_offer: subject.form_url_encoded_credential_offer, +// }]); +// } + +// #[test] +// #[serial_test::serial] +// fn test_create_token_response() { +// *PRE_AUTHORIZED_CODES.lock().unwrap() = vec![generate_random_string()].into(); +// *ACCESS_TOKENS.lock().unwrap() = vec![generate_random_string()].into(); +// *C_NONCES.lock().unwrap() = vec![generate_random_string()].into(); + +// let subject = test_subject(); +// OfferTestFramework::with(test_issuance_services()) +// .given(vec![ +// OfferEvent::CredentialOfferCreated { +// offer_id: Default::default(), +// pre_authorized_code: subject.pre_authorized_code.clone(), +// access_token: subject.access_token.clone(), +// }, +// OfferEvent::CredentialsAdded { +// offer_id: Default::default(), +// credential_ids: vec!["credential-id".to_string()], +// }, +// OfferEvent::FormUrlEncodedCredentialOfferCreated { +// offer_id: Default::default(), +// form_url_encoded_credential_offer: subject.form_url_encoded_credential_offer.clone(), +// }, +// ]) +// .when(OfferCommand::CreateTokenResponse { +// offer_id: Default::default(), +// token_request: token_request(subject.clone()), +// }) +// .then_expect_events(vec![OfferEvent::TokenResponseCreated { +// offer_id: Default::default(), +// token_response: token_response(subject), +// }]); +// } + +// #[rstest] +// #[serial_test::serial] +// async fn test_verify_credential_response() { +// *PRE_AUTHORIZED_CODES.lock().unwrap() = vec![generate_random_string()].into(); +// *ACCESS_TOKENS.lock().unwrap() = vec![generate_random_string()].into(); +// *C_NONCES.lock().unwrap() = vec![generate_random_string()].into(); + +// let subject = test_subject(); +// OfferTestFramework::with(test_issuance_services()) +// .given(vec![ +// OfferEvent::CredentialOfferCreated { +// offer_id: Default::default(), +// pre_authorized_code: subject.pre_authorized_code.clone(), +// access_token: subject.access_token.clone(), +// }, +// OfferEvent::CredentialsAdded { +// offer_id: Default::default(), +// credential_ids: vec!["credential-id".to_string()], +// }, +// OfferEvent::FormUrlEncodedCredentialOfferCreated { +// offer_id: Default::default(), +// form_url_encoded_credential_offer: subject.form_url_encoded_credential_offer.clone(), +// }, +// OfferEvent::TokenResponseCreated { +// offer_id: Default::default(), +// token_response: token_response(subject.clone()), +// }, +// ]) +// .when(OfferCommand::VerifyCredentialRequest { +// offer_id: Default::default(), +// credential_issuer_metadata: CREDENTIAL_ISSUER_METADATA.clone(), +// authorization_server_metadata: AUTHORIZATION_SERVER_METADATA.clone(), +// credential_request: credential_request(subject.clone()).await, +// }) +// .then_expect_events(vec![OfferEvent::CredentialRequestVerified { +// offer_id: Default::default(), +// subject_id: SUBJECT_KEY_DID.identifier("did:key", Algorithm::EdDSA).await.unwrap(), +// }]); +// } + +// #[rstest] +// #[serial_test::serial] +// async fn test_create_credential_response() { +// *PRE_AUTHORIZED_CODES.lock().unwrap() = vec![generate_random_string()].into(); +// *ACCESS_TOKENS.lock().unwrap() = vec![generate_random_string()].into(); +// *C_NONCES.lock().unwrap() = vec![generate_random_string()].into(); + +// let subject = test_subject(); +// OfferTestFramework::with(test_issuance_services()) +// .given(vec![ +// OfferEvent::CredentialOfferCreated { +// offer_id: Default::default(), +// pre_authorized_code: subject.pre_authorized_code.clone(), +// access_token: subject.access_token.clone(), +// }, +// OfferEvent::CredentialsAdded { +// offer_id: Default::default(), +// credential_ids: vec!["credential-id".to_string()], +// }, +// OfferEvent::FormUrlEncodedCredentialOfferCreated { +// offer_id: Default::default(), +// form_url_encoded_credential_offer: subject.form_url_encoded_credential_offer.clone(), +// }, +// OfferEvent::TokenResponseCreated { +// offer_id: Default::default(), +// token_response: token_response(subject.clone()), +// }, +// OfferEvent::CredentialRequestVerified { +// offer_id: Default::default(), +// subject_id: SUBJECT_KEY_DID.identifier("did:key", Algorithm::EdDSA).await.unwrap(), +// }, +// ]) +// .when(OfferCommand::CreateCredentialResponse { +// offer_id: Default::default(), +// signed_credentials: vec![json!(OPENBADGE_VERIFIABLE_CREDENTIAL_JWT)], +// }) +// .then_expect_events(vec![OfferEvent::CredentialResponseCreated { +// offer_id: Default::default(), +// credential_response: credential_response(subject), +// }]); +// } + +// #[derive(Clone)] +// struct TestSubject { +// subject: Arc, +// credential: String, +// access_token: String, +// pre_authorized_code: String, +// form_url_encoded_credential_offer: String, +// c_nonce: String, +// } + +// lazy_static! { +// pub static ref PRE_AUTHORIZED_CODES: Mutex> = Mutex::new(vec![].into()); +// pub static ref ACCESS_TOKENS: Mutex> = Mutex::new(vec![].into()); +// pub static ref C_NONCES: Mutex> = Mutex::new(vec![].into()); +// pub static ref SUBJECT_KEY_DID: Arc = test_issuance_services().issuer.clone(); +// } + +// fn test_subject() -> TestSubject { +// let pre_authorized_code = PRE_AUTHORIZED_CODES.lock().unwrap()[0].clone(); + +// TestSubject { +// subject: SUBJECT_KEY_DID.clone(), +// credential: OPENBADGE_VERIFIABLE_CREDENTIAL_JWT.to_string(), +// pre_authorized_code: pre_authorized_code.clone(), +// access_token: ACCESS_TOKENS.lock().unwrap()[0].clone(), +// form_url_encoded_credential_offer: format!("openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fexample.com%2F%22%2C%22credential_configuration_ids%22%3A%5B%220%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22{pre_authorized_code}%22%7D%7D%7D"), +// c_nonce: C_NONCES.lock().unwrap()[0].clone(), +// } +// } + +// fn token_request(subject: TestSubject) -> TokenRequest { +// TokenRequest::PreAuthorizedCode { +// pre_authorized_code: subject.pre_authorized_code, +// tx_code: None, +// } +// } + +// fn token_response(subject: TestSubject) -> TokenResponse { +// TokenResponse { +// access_token: subject.access_token.clone(), +// token_type: "bearer".to_string(), +// expires_in: None, +// refresh_token: None, +// scope: None, +// c_nonce: Some(subject.c_nonce.clone()), +// c_nonce_expires_in: None, +// } +// } + +// async fn credential_request(subject: TestSubject) -> CredentialRequest { +// CredentialRequest { +// credential_format: CredentialFormats::JwtVcJson(Parameters { +// parameters: ( +// CredentialDefinition { +// type_: vec!["VerifiableCredential".to_string(), "OpenBadgeCredential".to_string()], +// credential_subject: Default::default(), +// }, +// None, +// ) +// .into(), +// }), +// proof: Some( +// KeyProofType::builder() +// .proof_type(ProofType::Jwt) +// .algorithm(Algorithm::EdDSA) +// .signer(subject.subject.clone()) +// .iss(subject.subject.identifier("did:key", Algorithm::EdDSA).await.unwrap()) +// .aud(CREDENTIAL_ISSUER_METADATA.credential_issuer.clone()) +// .iat(1571324800) +// .nonce(subject.c_nonce.clone()) +// .subject_syntax_type("did:key") +// .build() +// .await +// .unwrap(), +// ), +// } +// } + +// fn credential_response(subject: TestSubject) -> CredentialResponse { +// CredentialResponse { +// credential: CredentialResponseType::Immediate { +// credential: json!(subject.credential.clone()), +// notification_id: None, +// }, +// c_nonce: None, +// c_nonce_expires_in: None, +// } +// } +// } diff --git a/agent_holder/src/offer/command.rs b/agent_holder/src/offer/command.rs new file mode 100644 index 00000000..2c5c3359 --- /dev/null +++ b/agent_holder/src/offer/command.rs @@ -0,0 +1,23 @@ +use oid4vci::credential_offer::CredentialOffer; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum OfferCommand { + ReceiveCredentialOffer { + offer_id: String, + credential_offer: CredentialOffer, + }, + AcceptCredentialOffer { + offer_id: String, + }, + SendTokenRequest { + offer_id: String, + }, + SendCredentialRequest { + offer_id: String, + }, + RejectCredentialOffer { + offer_id: String, + }, +} diff --git a/agent_holder/src/offer/error.rs b/agent_holder/src/offer/error.rs new file mode 100644 index 00000000..3cd038e7 --- /dev/null +++ b/agent_holder/src/offer/error.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum OfferError { + #[error("Credential is missing")] + MissingCredentialError, + #[error("Missing `Proof` in Credential Request")] + MissingProofError, + #[error("Invalid `Proof` in Credential Request")] + InvalidProofError(String), + #[error("Missing `iss` claim in `Proof`")] + MissingProofIssuerError, +} diff --git a/agent_holder/src/offer/event.rs b/agent_holder/src/offer/event.rs new file mode 100644 index 00000000..dcca3304 --- /dev/null +++ b/agent_holder/src/offer/event.rs @@ -0,0 +1,50 @@ +use std::collections::HashMap; + +use cqrs_es::DomainEvent; +use oid4vci::{ + credential_issuer::credential_configurations_supported::CredentialConfigurationsSupportedObject, + credential_offer::CredentialOfferParameters, token_response::TokenResponse, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum OfferEvent { + CredentialOfferReceived { + offer_id: String, + credential_offer: CredentialOfferParameters, + credential_configurations: HashMap, + }, + CredentialOfferAccepted { + offer_id: String, + }, + TokenResponseReceived { + offer_id: String, + token_response: TokenResponse, + }, + CredentialResponseReceived { + offer_id: String, + credentials: Vec, + }, + CredentialOfferRejected { + offer_id: String, + }, +} + +impl DomainEvent for OfferEvent { + fn event_type(&self) -> String { + use OfferEvent::*; + + let event_type: &str = match self { + CredentialOfferReceived { .. } => "CredentialOfferReceived", + CredentialOfferAccepted { .. } => "CredentialOfferAccepted", + TokenResponseReceived { .. } => "AccessTokenReceived", + CredentialResponseReceived { .. } => "CredentialResponseReceived", + CredentialOfferRejected { .. } => "CredentialOfferRejected", + }; + event_type.to_string() + } + + fn event_version(&self) -> String { + "1".to_string() + } +} diff --git a/agent_holder/src/offer/mod.rs b/agent_holder/src/offer/mod.rs new file mode 100644 index 00000000..7d8a943f --- /dev/null +++ b/agent_holder/src/offer/mod.rs @@ -0,0 +1,5 @@ +pub mod aggregate; +pub mod command; +pub mod error; +pub mod event; +pub mod queries; diff --git a/agent_holder/src/offer/queries/access_token.rs b/agent_holder/src/offer/queries/access_token.rs new file mode 100644 index 00000000..d25935f5 --- /dev/null +++ b/agent_holder/src/offer/queries/access_token.rs @@ -0,0 +1,87 @@ +use crate::offer::queries::{CustomQuery, Offer, OfferEvent, ViewRepository}; +use async_trait::async_trait; +use cqrs_es::{ + persist::{PersistenceError, ViewContext}, + EventEnvelope, Query, View, +}; +use serde::{Deserialize, Serialize}; +use std::marker::PhantomData; +use std::sync::Arc; + +/// A custom query trait for the Offer aggregate. This query is used to update the `AccessTokenView`. +pub struct AccessTokenQuery +where + R: ViewRepository, + V: View, +{ + view_repository: Arc, + _phantom: PhantomData, +} + +impl AccessTokenQuery +where + R: ViewRepository, + V: View, +{ + pub fn new(view_repository: Arc) -> Self { + AccessTokenQuery { + view_repository, + _phantom: PhantomData, + } + } +} + +#[async_trait] +impl Query for AccessTokenQuery +where + R: ViewRepository, + V: View, +{ + async fn dispatch(&self, view_id: &str, events: &[EventEnvelope]) { + self.apply_events(view_id, events).await.ok(); + } +} + +#[async_trait] +impl CustomQuery for AccessTokenQuery +where + R: ViewRepository, + V: View, +{ + async fn load_mut(&self, view_id: String) -> Result<(V, ViewContext), PersistenceError> { + match self.view_repository.load_with_context(&view_id).await? { + None => { + let view_context = ViewContext::new(view_id, 0); + Ok((Default::default(), view_context)) + } + Some((view, context)) => Ok((view, context)), + } + } + + async fn apply_events(&self, view_id: &str, events: &[EventEnvelope]) -> Result<(), PersistenceError> { + for event in events { + let (mut view, mut view_context) = self.load_mut(view_id.to_string()).await?; + if let OfferEvent::CredentialOfferCreated { access_token, .. } = &event.payload { + view_context.view_instance_id.clone_from(access_token); + view.update(event); + self.view_repository.update_view(view, view_context).await?; + } + } + Ok(()) + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct AccessTokenView { + pub offer_id: String, +} + +impl View for AccessTokenView { + fn update(&mut self, event: &EventEnvelope) { + use crate::offer::event::OfferEvent::*; + + if let CredentialOfferCreated { .. } = event.payload { + self.offer_id.clone_from(&event.aggregate_id) + } + } +} diff --git a/agent_holder/src/offer/queries/mod.rs b/agent_holder/src/offer/queries/mod.rs new file mode 100644 index 00000000..35180e30 --- /dev/null +++ b/agent_holder/src/offer/queries/mod.rs @@ -0,0 +1,67 @@ +// pub mod access_token; +// pub mod pre_authorized_code; + +use std::collections::HashMap; + +use async_trait::async_trait; +use cqrs_es::{ + persist::{PersistenceError, ViewContext, ViewRepository}, + EventEnvelope, Query, View, +}; +use oid4vci::{ + credential_issuer::credential_configurations_supported::CredentialConfigurationsSupportedObject, + credential_offer::CredentialOfferParameters, credential_response::CredentialResponse, + token_response::TokenResponse, +}; +use serde::{Deserialize, Serialize}; + +use crate::offer::aggregate::Offer; + +use super::event::OfferEvent; + +/// A custom query trait for the Offer aggregate. This trait is used to define custom queries for the Offer aggregate +/// that do not make use of `GenericQuery`. +#[async_trait] +pub trait CustomQuery: Query +where + R: ViewRepository, + V: View, +{ + async fn load_mut(&self, view_id: String) -> Result<(V, ViewContext), PersistenceError>; + + async fn apply_events(&self, view_id: &str, events: &[EventEnvelope]) -> Result<(), PersistenceError>; +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct OfferView { + pub credential_offer: Option, + pub credential_configurations: Option>, + pub token_response: Option, + pub credentials: Vec, +} + +impl View for OfferView { + fn update(&mut self, event: &EventEnvelope) { + use crate::offer::event::OfferEvent::*; + + match &event.payload { + CredentialOfferReceived { + credential_offer, + credential_configurations, + .. + } => { + self.credential_offer.replace(credential_offer.clone()); + self.credential_configurations + .replace(credential_configurations.clone()); + } + CredentialOfferAccepted { .. } => {} + TokenResponseReceived { token_response, .. } => { + self.token_response.replace(token_response.clone()); + } + CredentialResponseReceived { credentials, .. } => { + self.credentials.extend(credentials.clone()); + } + CredentialOfferRejected { .. } => todo!(), + } + } +} diff --git a/agent_holder/src/offer/queries/pre_authorized_code.rs b/agent_holder/src/offer/queries/pre_authorized_code.rs new file mode 100644 index 00000000..2f96bd13 --- /dev/null +++ b/agent_holder/src/offer/queries/pre_authorized_code.rs @@ -0,0 +1,90 @@ +use crate::offer::queries::{CustomQuery, Offer, OfferEvent, ViewRepository}; +use async_trait::async_trait; +use cqrs_es::{ + persist::{PersistenceError, ViewContext}, + EventEnvelope, Query, View, +}; +use serde::{Deserialize, Serialize}; +use std::marker::PhantomData; +use std::sync::Arc; + +/// A custom query trait for the Offer aggregate. This query is used to update the `PreAuthorizedCodeView`. +pub struct PreAuthorizedCodeQuery +where + R: ViewRepository, + V: View, +{ + view_repository: Arc, + _phantom: PhantomData, +} + +impl PreAuthorizedCodeQuery +where + R: ViewRepository, + V: View, +{ + pub fn new(view_repository: Arc) -> Self { + PreAuthorizedCodeQuery { + view_repository, + _phantom: PhantomData, + } + } +} + +#[async_trait] +impl Query for PreAuthorizedCodeQuery +where + R: ViewRepository, + V: View, +{ + async fn dispatch(&self, view_id: &str, events: &[EventEnvelope]) { + self.apply_events(view_id, events).await.ok(); + } +} + +#[async_trait] +impl CustomQuery for PreAuthorizedCodeQuery +where + R: ViewRepository, + V: View, +{ + async fn load_mut(&self, view_id: String) -> Result<(V, ViewContext), PersistenceError> { + match self.view_repository.load_with_context(&view_id).await? { + None => { + let view_context = ViewContext::new(view_id, 0); + Ok((Default::default(), view_context)) + } + Some((view, context)) => Ok((view, context)), + } + } + + async fn apply_events(&self, view_id: &str, events: &[EventEnvelope]) -> Result<(), PersistenceError> { + for event in events { + let (mut view, mut view_context) = self.load_mut(view_id.to_string()).await?; + if let OfferEvent::CredentialOfferCreated { + pre_authorized_code, .. + } = &event.payload + { + view_context.view_instance_id.clone_from(pre_authorized_code); + view.update(event); + self.view_repository.update_view(view, view_context).await?; + } + } + Ok(()) + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct PreAuthorizedCodeView { + pub offer_id: String, +} + +impl View for PreAuthorizedCodeView { + fn update(&mut self, event: &EventEnvelope) { + use crate::offer::event::OfferEvent::*; + + if let CredentialOfferCreated { .. } = event.payload { + self.offer_id.clone_from(&event.aggregate_id) + } + } +} diff --git a/agent_holder/src/services.rs b/agent_holder/src/services.rs new file mode 100644 index 00000000..70e17497 --- /dev/null +++ b/agent_holder/src/services.rs @@ -0,0 +1,64 @@ +use agent_shared::config::{config, get_all_enabled_did_methods, get_preferred_did_method}; +use jsonwebtoken::Algorithm; +use oid4vc_core::{Subject, SubjectSyntaxType}; +use oid4vci::Wallet; +use std::{str::FromStr, sync::Arc}; + +/// Holder services. This struct is used to sign credentials and validate credential requests. +pub struct HolderServices { + pub holder: Arc, + pub wallet: Wallet, +} + +impl HolderServices { + pub fn new(holder: Arc) -> Self { + let signing_algorithms_supported: Vec = config() + .signing_algorithms_supported + .iter() + .filter(|(_, opts)| opts.enabled) + .map(|(alg, _)| *alg) + .collect(); + + let mut enabled_did_methods = get_all_enabled_did_methods(); + let preferred_did_method = get_preferred_did_method(); + enabled_did_methods.sort_by(|a, b| { + if *a == preferred_did_method { + std::cmp::Ordering::Less + } else if *b == preferred_did_method { + std::cmp::Ordering::Greater + } else { + std::cmp::Ordering::Equal + } + }); + + let supported_subject_syntax_types = enabled_did_methods + .into_iter() + .map(|method| SubjectSyntaxType::from_str(&method.to_string()).unwrap()) + .collect(); + + let wallet = Wallet::new( + holder.clone(), + supported_subject_syntax_types, + signing_algorithms_supported, + ) + .unwrap(); + + Self { holder, wallet } + } +} + +#[cfg(feature = "test_utils")] +pub mod test_utils { + use agent_secret_manager::secret_manager; + use agent_secret_manager::subject::Subject; + + use super::*; + + pub fn test_holder_services() -> Arc { + Arc::new(HolderServices::new(Arc::new(futures::executor::block_on(async { + Subject { + secret_manager: secret_manager().await, + } + })))) + } +} From ebfc36d6aa5d50bf30734e9e7f2fb95783686eb9 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Mon, 26 Aug 2024 08:22:43 +0200 Subject: [PATCH 02/31] feat: add `HolderState` --- agent_holder/src/lib.rs | 1 + .../src/offer/{queries/mod.rs => queries.rs} | 0 .../src/offer/queries/access_token.rs | 87 ------------------ .../src/offer/queries/pre_authorized_code.rs | 90 ------------------- agent_holder/src/state.rs | 51 +++++++++++ agent_issuance/src/state.rs | 4 +- agent_verification/src/state.rs | 6 +- 7 files changed, 57 insertions(+), 182 deletions(-) rename agent_holder/src/offer/{queries/mod.rs => queries.rs} (100%) delete mode 100644 agent_holder/src/offer/queries/access_token.rs delete mode 100644 agent_holder/src/offer/queries/pre_authorized_code.rs create mode 100644 agent_holder/src/state.rs diff --git a/agent_holder/src/lib.rs b/agent_holder/src/lib.rs index 185af944..671c165e 100644 --- a/agent_holder/src/lib.rs +++ b/agent_holder/src/lib.rs @@ -1,3 +1,4 @@ pub mod credential; pub mod offer; pub mod services; +pub mod state; diff --git a/agent_holder/src/offer/queries/mod.rs b/agent_holder/src/offer/queries.rs similarity index 100% rename from agent_holder/src/offer/queries/mod.rs rename to agent_holder/src/offer/queries.rs diff --git a/agent_holder/src/offer/queries/access_token.rs b/agent_holder/src/offer/queries/access_token.rs deleted file mode 100644 index d25935f5..00000000 --- a/agent_holder/src/offer/queries/access_token.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::offer::queries::{CustomQuery, Offer, OfferEvent, ViewRepository}; -use async_trait::async_trait; -use cqrs_es::{ - persist::{PersistenceError, ViewContext}, - EventEnvelope, Query, View, -}; -use serde::{Deserialize, Serialize}; -use std::marker::PhantomData; -use std::sync::Arc; - -/// A custom query trait for the Offer aggregate. This query is used to update the `AccessTokenView`. -pub struct AccessTokenQuery -where - R: ViewRepository, - V: View, -{ - view_repository: Arc, - _phantom: PhantomData, -} - -impl AccessTokenQuery -where - R: ViewRepository, - V: View, -{ - pub fn new(view_repository: Arc) -> Self { - AccessTokenQuery { - view_repository, - _phantom: PhantomData, - } - } -} - -#[async_trait] -impl Query for AccessTokenQuery -where - R: ViewRepository, - V: View, -{ - async fn dispatch(&self, view_id: &str, events: &[EventEnvelope]) { - self.apply_events(view_id, events).await.ok(); - } -} - -#[async_trait] -impl CustomQuery for AccessTokenQuery -where - R: ViewRepository, - V: View, -{ - async fn load_mut(&self, view_id: String) -> Result<(V, ViewContext), PersistenceError> { - match self.view_repository.load_with_context(&view_id).await? { - None => { - let view_context = ViewContext::new(view_id, 0); - Ok((Default::default(), view_context)) - } - Some((view, context)) => Ok((view, context)), - } - } - - async fn apply_events(&self, view_id: &str, events: &[EventEnvelope]) -> Result<(), PersistenceError> { - for event in events { - let (mut view, mut view_context) = self.load_mut(view_id.to_string()).await?; - if let OfferEvent::CredentialOfferCreated { access_token, .. } = &event.payload { - view_context.view_instance_id.clone_from(access_token); - view.update(event); - self.view_repository.update_view(view, view_context).await?; - } - } - Ok(()) - } -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct AccessTokenView { - pub offer_id: String, -} - -impl View for AccessTokenView { - fn update(&mut self, event: &EventEnvelope) { - use crate::offer::event::OfferEvent::*; - - if let CredentialOfferCreated { .. } = event.payload { - self.offer_id.clone_from(&event.aggregate_id) - } - } -} diff --git a/agent_holder/src/offer/queries/pre_authorized_code.rs b/agent_holder/src/offer/queries/pre_authorized_code.rs deleted file mode 100644 index 2f96bd13..00000000 --- a/agent_holder/src/offer/queries/pre_authorized_code.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::offer::queries::{CustomQuery, Offer, OfferEvent, ViewRepository}; -use async_trait::async_trait; -use cqrs_es::{ - persist::{PersistenceError, ViewContext}, - EventEnvelope, Query, View, -}; -use serde::{Deserialize, Serialize}; -use std::marker::PhantomData; -use std::sync::Arc; - -/// A custom query trait for the Offer aggregate. This query is used to update the `PreAuthorizedCodeView`. -pub struct PreAuthorizedCodeQuery -where - R: ViewRepository, - V: View, -{ - view_repository: Arc, - _phantom: PhantomData, -} - -impl PreAuthorizedCodeQuery -where - R: ViewRepository, - V: View, -{ - pub fn new(view_repository: Arc) -> Self { - PreAuthorizedCodeQuery { - view_repository, - _phantom: PhantomData, - } - } -} - -#[async_trait] -impl Query for PreAuthorizedCodeQuery -where - R: ViewRepository, - V: View, -{ - async fn dispatch(&self, view_id: &str, events: &[EventEnvelope]) { - self.apply_events(view_id, events).await.ok(); - } -} - -#[async_trait] -impl CustomQuery for PreAuthorizedCodeQuery -where - R: ViewRepository, - V: View, -{ - async fn load_mut(&self, view_id: String) -> Result<(V, ViewContext), PersistenceError> { - match self.view_repository.load_with_context(&view_id).await? { - None => { - let view_context = ViewContext::new(view_id, 0); - Ok((Default::default(), view_context)) - } - Some((view, context)) => Ok((view, context)), - } - } - - async fn apply_events(&self, view_id: &str, events: &[EventEnvelope]) -> Result<(), PersistenceError> { - for event in events { - let (mut view, mut view_context) = self.load_mut(view_id.to_string()).await?; - if let OfferEvent::CredentialOfferCreated { - pre_authorized_code, .. - } = &event.payload - { - view_context.view_instance_id.clone_from(pre_authorized_code); - view.update(event); - self.view_repository.update_view(view, view_context).await?; - } - } - Ok(()) - } -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct PreAuthorizedCodeView { - pub offer_id: String, -} - -impl View for PreAuthorizedCodeView { - fn update(&mut self, event: &EventEnvelope) { - use crate::offer::event::OfferEvent::*; - - if let CredentialOfferCreated { .. } = event.payload { - self.offer_id.clone_from(&event.aggregate_id) - } - } -} diff --git a/agent_holder/src/state.rs b/agent_holder/src/state.rs new file mode 100644 index 00000000..de06d0bd --- /dev/null +++ b/agent_holder/src/state.rs @@ -0,0 +1,51 @@ +use agent_shared::application_state::CommandHandler; +use cqrs_es::persist::ViewRepository; +use std::sync::Arc; + +use crate::credential::aggregate::Credential; +use crate::credential::queries::CredentialView; +use crate::offer::aggregate::Offer; +use crate::offer::queries::OfferView; +use axum::extract::FromRef; + +#[derive(Clone)] +pub struct HolderState { + pub command: CommandHandlers, + pub query: Queries, +} + +impl FromRef<(I, HolderState, V)> for HolderState { + fn from_ref(application_state: &(I, HolderState, V)) -> HolderState { + application_state.1.clone() + } +} + +/// The command handlers are used to execute commands on the aggregates. +#[derive(Clone)] +pub struct CommandHandlers { + pub credential: CommandHandler, + pub offer: CommandHandler, +} + +/// This type is used to define the queries that are used to query the view repositories. We make use of `dyn` here, so +/// that any type of repository that implements the `ViewRepository` trait can be used, but the corresponding `View` and +/// `Aggregate` types must be the same. +type Queries = ViewRepositories, dyn ViewRepository>; + +pub struct ViewRepositories +where + C: ViewRepository + ?Sized, + O: ViewRepository + ?Sized, +{ + pub credential: Arc, + pub offer: Arc, +} + +impl Clone for Queries { + fn clone(&self) -> Self { + ViewRepositories { + credential: self.credential.clone(), + offer: self.offer.clone(), + } + } +} diff --git a/agent_issuance/src/state.rs b/agent_issuance/src/state.rs index 7f372c42..a964a764 100644 --- a/agent_issuance/src/state.rs +++ b/agent_issuance/src/state.rs @@ -21,8 +21,8 @@ pub struct IssuanceState { pub query: Queries, } -impl FromRef<(IssuanceState, V)> for IssuanceState { - fn from_ref(application_state: &(IssuanceState, V)) -> IssuanceState { +impl FromRef<(IssuanceState, H, V)> for IssuanceState { + fn from_ref(application_state: &(IssuanceState, H, V)) -> IssuanceState { application_state.0.clone() } } diff --git a/agent_verification/src/state.rs b/agent_verification/src/state.rs index 21c7fe62..03f2570a 100644 --- a/agent_verification/src/state.rs +++ b/agent_verification/src/state.rs @@ -15,9 +15,9 @@ pub struct VerificationState { pub query: Queries, } -impl FromRef<(I, VerificationState)> for VerificationState { - fn from_ref(application_state: &(I, VerificationState)) -> VerificationState { - application_state.1.clone() +impl FromRef<(I, H, VerificationState)> for VerificationState { + fn from_ref(application_state: &(I, H, VerificationState)) -> VerificationState { + application_state.2.clone() } } From bca826ebe505b6039b8def80ed4d59d94c40c950 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Mon, 26 Aug 2024 08:55:54 +0200 Subject: [PATCH 03/31] feat: add Holder functionality to `agent_store` and `agent_api_rest` --- Cargo.lock | 3 ++ agent_api_rest/Cargo.toml | 2 + .../issuance/credential_issuer/credential.rs | 46 ++++++++++--------- .../src/issuance/credential_issuer/token.rs | 4 +- .../well_known/oauth_authorization_server.rs | 4 +- .../well_known/openid_credential_issuer.rs | 4 +- agent_api_rest/src/issuance/credentials.rs | 4 +- agent_api_rest/src/issuance/offers.rs | 4 +- agent_api_rest/src/lib.rs | 8 +++- .../verification/authorization_requests.rs | 4 +- .../verification/relying_party/redirect.rs | 4 +- .../src/verification/relying_party/request.rs | 4 +- agent_application/Cargo.toml | 1 + agent_application/src/main.rs | 15 ++++-- agent_store/Cargo.toml | 1 + agent_store/src/in_memory.rs | 39 +++++++++++++++- agent_store/src/postgres.rs | 41 +++++++++++++++++ 17 files changed, 150 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7aaedc6a..bc6e9c05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,6 +63,7 @@ name = "agent_api_rest" version = "0.1.0" dependencies = [ "agent_event_publisher_http", + "agent_holder", "agent_issuance", "agent_secret_manager", "agent_shared", @@ -105,6 +106,7 @@ version = "0.1.0" dependencies = [ "agent_api_rest", "agent_event_publisher_http", + "agent_holder", "agent_issuance", "agent_secret_manager", "agent_shared", @@ -270,6 +272,7 @@ dependencies = [ name = "agent_store" version = "0.1.0" dependencies = [ + "agent_holder", "agent_issuance", "agent_shared", "agent_verification", diff --git a/agent_api_rest/Cargo.toml b/agent_api_rest/Cargo.toml index 88210ade..48b80cab 100644 --- a/agent_api_rest/Cargo.toml +++ b/agent_api_rest/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true rust-version.workspace = true [dependencies] +agent_holder = { path = "../agent_holder" } agent_issuance = { path = "../agent_issuance" } agent_shared = { path = "../agent_shared" } agent_verification = { path = "../agent_verification" } @@ -28,6 +29,7 @@ uuid.workspace = true [dev-dependencies] agent_event_publisher_http = { path = "../agent_event_publisher_http", features = ["test_utils"] } +agent_holder = { path = "../agent_holder", features = ["test_utils"] } agent_issuance = { path = "../agent_issuance", features = ["test_utils"] } agent_secret_manager = { path = "../agent_secret_manager" } agent_shared = { path = "../agent_shared", features = ["test_utils"] } diff --git a/agent_api_rest/src/issuance/credential_issuer/credential.rs b/agent_api_rest/src/issuance/credential_issuer/credential.rs index 9cda7ee3..91571bce 100644 --- a/agent_api_rest/src/issuance/credential_issuer/credential.rs +++ b/agent_api_rest/src/issuance/credential_issuer/credential.rs @@ -155,6 +155,7 @@ mod tests { use crate::issuance::credentials::tests::credentials; use crate::API_VERSION; use agent_event_publisher_http::EventPublisherHttp; + use agent_holder::services::test_utils::test_holder_services; use agent_issuance::services::test_utils::test_issuance_services; use agent_issuance::{offer::event::OfferEvent, startup_commands::startup_commands, state::initialize}; use agent_shared::config::{set_config, Events}; @@ -277,33 +278,36 @@ mod tests { #[case] is_self_signed: bool, #[case] delay: u64, ) { - let (external_server, issuance_event_publishers, verification_event_publishers) = if with_external_server { - let external_server = MockServer::start().await; - - let target_url = format!("{}/ssi-events-subscriber", &external_server.uri()); - - set_config().enable_event_publisher_http(); - set_config().set_event_publisher_http_target_url(target_url.clone()); - set_config().set_event_publisher_http_target_events(Events { - offer: vec![agent_shared::config::OfferEvent::CredentialRequestVerified], - ..Default::default() - }); - - ( - Some(external_server), - vec![Box::new(EventPublisherHttp::load().unwrap()) as Box], - vec![Box::new(EventPublisherHttp::load().unwrap()) as Box], - ) - } else { - (None, Default::default(), Default::default()) - }; + let (external_server, issuance_event_publishers, holder_event_publishers, verification_event_publishers) = + if with_external_server { + let external_server = MockServer::start().await; + + let target_url = format!("{}/ssi-events-subscriber", &external_server.uri()); + + set_config().enable_event_publisher_http(); + set_config().set_event_publisher_http_target_url(target_url.clone()); + set_config().set_event_publisher_http_target_events(Events { + offer: vec![agent_shared::config::OfferEvent::CredentialRequestVerified], + ..Default::default() + }); + + ( + Some(external_server), + vec![Box::new(EventPublisherHttp::load().unwrap()) as Box], + vec![Box::new(EventPublisherHttp::load().unwrap()) as Box], + vec![Box::new(EventPublisherHttp::load().unwrap()) as Box], + ) + } else { + (None, Default::default(), Default::default(), Default::default()) + }; let issuance_state = in_memory::issuance_state(test_issuance_services(), issuance_event_publishers).await; + let holder_state = in_memory::holder_state(test_holder_services(), holder_event_publishers).await; let verification_state = in_memory::verification_state(test_verification_services(), verification_event_publishers).await; initialize(&issuance_state, startup_commands(BASE_URL.clone())).await; - let mut app = app((issuance_state, verification_state)); + let mut app = app((issuance_state, holder_state, verification_state)); if let Some(external_server) = &external_server { external_server diff --git a/agent_api_rest/src/issuance/credential_issuer/token.rs b/agent_api_rest/src/issuance/credential_issuer/token.rs index 91ad4279..a9ec9154 100644 --- a/agent_api_rest/src/issuance/credential_issuer/token.rs +++ b/agent_api_rest/src/issuance/credential_issuer/token.rs @@ -67,6 +67,7 @@ pub mod tests { }; use super::*; + use agent_holder::services::test_utils::test_holder_services; use agent_issuance::{ services::test_utils::test_issuance_services, startup_commands::startup_commands, state::initialize, }; @@ -113,10 +114,11 @@ pub mod tests { #[tokio::test] async fn test_token_endpoint() { let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; + let holder_state = in_memory::holder_state(test_holder_services(), Default::default()).await; let verification_state = in_memory::verification_state(test_verification_services(), Default::default()).await; initialize(&issuance_state, startup_commands(BASE_URL.clone())).await; - let mut app = app((issuance_state, verification_state)); + let mut app = app((issuance_state, holder_state, verification_state)); credentials(&mut app).await; let pre_authorized_code = offers(&mut app).await; diff --git a/agent_api_rest/src/issuance/credential_issuer/well_known/oauth_authorization_server.rs b/agent_api_rest/src/issuance/credential_issuer/well_known/oauth_authorization_server.rs index c690064d..e40c8fb2 100644 --- a/agent_api_rest/src/issuance/credential_issuer/well_known/oauth_authorization_server.rs +++ b/agent_api_rest/src/issuance/credential_issuer/well_known/oauth_authorization_server.rs @@ -26,6 +26,7 @@ mod tests { use crate::{app, tests::BASE_URL}; use super::*; + use agent_holder::services::test_utils::test_holder_services; use agent_issuance::{ services::test_utils::test_issuance_services, startup_commands::startup_commands, state::initialize, }; @@ -73,10 +74,11 @@ mod tests { #[tokio::test] async fn test_oauth_authorization_server_endpoint() { let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; + let holder_state = in_memory::holder_state(test_holder_services(), Default::default()).await; let verification_state = in_memory::verification_state(test_verification_services(), Default::default()).await; initialize(&issuance_state, startup_commands(BASE_URL.clone())).await; - let mut app = app((issuance_state, verification_state)); + let mut app = app((issuance_state, holder_state, verification_state)); let _authorization_server_metadata = oauth_authorization_server(&mut app).await; } diff --git a/agent_api_rest/src/issuance/credential_issuer/well_known/openid_credential_issuer.rs b/agent_api_rest/src/issuance/credential_issuer/well_known/openid_credential_issuer.rs index 2f93a878..7a28fda4 100644 --- a/agent_api_rest/src/issuance/credential_issuer/well_known/openid_credential_issuer.rs +++ b/agent_api_rest/src/issuance/credential_issuer/well_known/openid_credential_issuer.rs @@ -28,6 +28,7 @@ mod tests { use crate::{app, tests::BASE_URL}; use super::*; + use agent_holder::services::test_utils::test_holder_services; use agent_issuance::{ services::test_utils::test_issuance_services, startup_commands::startup_commands, state::initialize, }; @@ -134,10 +135,11 @@ mod tests { #[tokio::test] async fn test_openid_credential_issuer_endpoint() { let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; + let holder_state = in_memory::holder_state(test_holder_services(), Default::default()).await; let verification_state = in_memory::verification_state(test_verification_services(), Default::default()).await; initialize(&issuance_state, startup_commands(BASE_URL.clone())).await; - let mut app = app((issuance_state, verification_state)); + let mut app = app((issuance_state, holder_state, verification_state)); let _credential_issuer_metadata = openid_credential_issuer(&mut app).await; } diff --git a/agent_api_rest/src/issuance/credentials.rs b/agent_api_rest/src/issuance/credentials.rs index 2689f27c..d2f4ca1c 100644 --- a/agent_api_rest/src/issuance/credentials.rs +++ b/agent_api_rest/src/issuance/credentials.rs @@ -158,6 +158,7 @@ pub mod tests { app, tests::{BASE_URL, CREDENTIAL_CONFIGURATION_ID, OFFER_ID}, }; + use agent_holder::services::test_utils::test_holder_services; use agent_issuance::services::test_utils::test_issuance_services; use agent_issuance::{startup_commands::startup_commands, state::initialize}; use agent_store::in_memory; @@ -254,10 +255,11 @@ pub mod tests { #[tracing_test::traced_test] async fn test_credentials_endpoint() { let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; + let holder_state = in_memory::holder_state(test_holder_services(), Default::default()).await; let verification_state = in_memory::verification_state(test_verification_services(), Default::default()).await; initialize(&issuance_state, startup_commands(BASE_URL.clone())).await; - let mut app = app((issuance_state, verification_state)); + let mut app = app((issuance_state, holder_state, verification_state)); credentials(&mut app).await; } diff --git a/agent_api_rest/src/issuance/offers.rs b/agent_api_rest/src/issuance/offers.rs index be3ea8d7..fa78ea2a 100644 --- a/agent_api_rest/src/issuance/offers.rs +++ b/agent_api_rest/src/issuance/offers.rs @@ -91,6 +91,7 @@ pub mod tests { use super::*; use crate::API_VERSION; + use agent_holder::services::test_utils::test_holder_services; use agent_issuance::{ services::test_utils::test_issuance_services, startup_commands::startup_commands, state::initialize, }; @@ -157,12 +158,13 @@ pub mod tests { #[tracing_test::traced_test] async fn test_offers_endpoint() { let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; + let holder_state = in_memory::holder_state(test_holder_services(), Default::default()).await; let verification_state = in_memory::verification_state(test_verification_services(), Default::default()).await; initialize(&issuance_state, startup_commands(BASE_URL.clone())).await; - let mut app = app((issuance_state, verification_state)); + let mut app = app((issuance_state, holder_state, verification_state)); credentials(&mut app).await; let _pre_authorized_code = offers(&mut app).await; diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index d2d72f1d..d18beebf 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -1,6 +1,8 @@ +mod holder; mod issuance; mod verification; +use agent_holder::state::HolderState; use agent_issuance::state::IssuanceState; use agent_shared::{config::config, ConfigError}; use agent_verification::state::VerificationState; @@ -30,7 +32,7 @@ use verification::{ pub const API_VERSION: &str = "/v0"; -pub type ApplicationState = (IssuanceState, VerificationState); +pub type ApplicationState = (IssuanceState, HolderState, VerificationState); pub fn app(state: ApplicationState) -> Router { let base_path = get_base_path(); @@ -126,6 +128,7 @@ fn get_base_path() -> Result { mod tests { use std::collections::HashMap; + use agent_holder::services::test_utils::test_holder_services; use agent_issuance::services::test_utils::test_issuance_services; use agent_store::in_memory; use agent_verification::services::test_utils::test_verification_services; @@ -184,9 +187,10 @@ mod tests { #[should_panic] async fn test_base_path_routes() { let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; + let holder_state = in_memory::holder_state(test_holder_services(), Default::default()).await; let verification_state = in_memory::verification_state(test_verification_services(), Default::default()).await; std::env::set_var("UNICORE__BASE_PATH", "unicore"); - let router = app((issuance_state, verification_state)); + let router = app((issuance_state, holder_state, verification_state)); let _ = router.route("/auth/token", post(handler)); } diff --git a/agent_api_rest/src/verification/authorization_requests.rs b/agent_api_rest/src/verification/authorization_requests.rs index ad79ff15..7ef10126 100644 --- a/agent_api_rest/src/verification/authorization_requests.rs +++ b/agent_api_rest/src/verification/authorization_requests.rs @@ -139,6 +139,7 @@ pub(crate) async fn authorization_requests( pub mod tests { use super::*; use crate::app; + use agent_holder::services::test_utils::test_holder_services; use agent_issuance::services::test_utils::test_issuance_services; use agent_store::in_memory; use agent_verification::services::test_utils::test_verification_services; @@ -222,8 +223,9 @@ pub mod tests { #[tracing_test::traced_test] async fn test_authorization_requests_endpoint(#[case] by_value: bool) { let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; + let holder_state = in_memory::holder_state(test_holder_services(), Default::default()).await; let verification_state = in_memory::verification_state(test_verification_services(), Default::default()).await; - let mut app = app((issuance_state, verification_state)); + let mut app = app((issuance_state, holder_state, verification_state)); authorization_requests(&mut app, by_value).await; } diff --git a/agent_api_rest/src/verification/relying_party/redirect.rs b/agent_api_rest/src/verification/relying_party/redirect.rs index 53e475b3..fb8ddf4e 100644 --- a/agent_api_rest/src/verification/relying_party/redirect.rs +++ b/agent_api_rest/src/verification/relying_party/redirect.rs @@ -62,6 +62,7 @@ pub mod tests { verification::{authorization_requests::tests::authorization_requests, relying_party::request::tests::request}, }; use agent_event_publisher_http::EventPublisherHttp; + use agent_holder::services::test_utils::test_holder_services; use agent_issuance::services::test_utils::test_issuance_services; use agent_secret_manager::{secret_manager, subject::Subject}; use agent_shared::config::{set_config, Events}; @@ -163,9 +164,10 @@ pub mod tests { let event_publishers = vec![Box::new(EventPublisherHttp::load().unwrap()) as Box]; let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; + let holder_state = in_memory::holder_state(test_holder_services(), Default::default()).await; let verification_state = in_memory::verification_state(test_verification_services(), event_publishers).await; - let mut app = app((issuance_state, verification_state)); + let mut app = app((issuance_state, holder_state, verification_state)); let form_url_encoded_authorization_request = authorization_requests(&mut app, false).await; diff --git a/agent_api_rest/src/verification/relying_party/request.rs b/agent_api_rest/src/verification/relying_party/request.rs index 4ddcb24b..7d98010c 100644 --- a/agent_api_rest/src/verification/relying_party/request.rs +++ b/agent_api_rest/src/verification/relying_party/request.rs @@ -34,6 +34,7 @@ pub(crate) async fn request( pub mod tests { use super::*; use crate::{app, verification::authorization_requests::tests::authorization_requests}; + use agent_holder::services::test_utils::test_holder_services; use agent_issuance::services::test_utils::test_issuance_services; use agent_store::in_memory; use agent_verification::services::test_utils::test_verification_services; @@ -71,8 +72,9 @@ pub mod tests { #[tracing_test::traced_test] async fn test_request_endpoint() { let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; + let holder_state = in_memory::holder_state(test_holder_services(), Default::default()).await; let verification_state = in_memory::verification_state(test_verification_services(), Default::default()).await; - let mut app = app((issuance_state, verification_state)); + let mut app = app((issuance_state, holder_state, verification_state)); let form_url_encoded_authorization_request = authorization_requests(&mut app, false).await; diff --git a/agent_application/Cargo.toml b/agent_application/Cargo.toml index 6dc41cbb..13457126 100644 --- a/agent_application/Cargo.toml +++ b/agent_application/Cargo.toml @@ -7,6 +7,7 @@ rust-version.workspace = true [dependencies] agent_api_rest = { path = "../agent_api_rest" } agent_event_publisher_http = { path = "../agent_event_publisher_http" } +agent_holder = { path = "../agent_holder" } agent_issuance = { path = "../agent_issuance" } agent_secret_manager = { path = "../agent_secret_manager" } agent_shared = { path = "../agent_shared" } diff --git a/agent_application/src/main.rs b/agent_application/src/main.rs index a299d40c..697d276a 100644 --- a/agent_application/src/main.rs +++ b/agent_application/src/main.rs @@ -2,6 +2,7 @@ use agent_api_rest::app; use agent_event_publisher_http::EventPublisherHttp; +use agent_holder::services::HolderServices; use agent_issuance::{services::IssuanceServices, startup_commands::startup_commands, state::initialize}; use agent_secret_manager::{secret_manager, subject::Subject}; use agent_shared::{ @@ -35,22 +36,26 @@ async fn main() -> io::Result<()> { }); let issuance_services = Arc::new(IssuanceServices::new(subject.clone())); + let holder_services = Arc::new(HolderServices::new(subject.clone())); let verification_services = Arc::new(VerificationServices::new(subject.clone())); - // TODO: Currently `issuance_event_publishers` and `verification_event_publishers` are exactly the same, which is - // weird. We need some sort of layer between `agent_application` and `agent_store` that will provide a cleaner way - // of initializing the event publishers and sending them over to `agent_store`. + // TODO: Currently `issuance_event_publishers`, `holder_event_publishers` and `verification_event_publishers` are + // exactly the same, which is weird. We need some sort of layer between `agent_application` and `agent_store` that + // will provide a cleaner way of initializing the event publishers and sending them over to `agent_store`. let issuance_event_publishers: Vec> = vec![Box::new(EventPublisherHttp::load().unwrap())]; + let holder_event_publishers: Vec> = vec![Box::new(EventPublisherHttp::load().unwrap())]; let verification_event_publishers: Vec> = vec![Box::new(EventPublisherHttp::load().unwrap())]; - let (issuance_state, verification_state) = match agent_shared::config::config().event_store.type_ { + let (issuance_state, holder_state, verification_state) = match agent_shared::config::config().event_store.type_ { agent_shared::config::EventStoreType::Postgres => ( postgres::issuance_state(issuance_services, issuance_event_publishers).await, + postgres::holder_state(holder_services, holder_event_publishers).await, postgres::verification_state(verification_services, verification_event_publishers).await, ), agent_shared::config::EventStoreType::InMemory => ( in_memory::issuance_state(issuance_services, issuance_event_publishers).await, + in_memory::holder_state(holder_services, holder_event_publishers).await, in_memory::verification_state(verification_services, verification_event_publishers).await, ), }; @@ -65,7 +70,7 @@ async fn main() -> io::Result<()> { initialize(&issuance_state, startup_commands(url.clone())).await; - let mut app = app((issuance_state, verification_state)); + let mut app = app((issuance_state, holder_state, verification_state)); // CORS if config().cors_enabled.unwrap_or(false) { diff --git a/agent_store/Cargo.toml b/agent_store/Cargo.toml index 05eafeae..90b86f95 100644 --- a/agent_store/Cargo.toml +++ b/agent_store/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true rust-version.workspace = true [dependencies] +agent_holder = { path = "../agent_holder" } agent_issuance = { path = "../agent_issuance" } agent_shared = { path = "../agent_shared" } agent_verification = { path = "../agent_verification" } diff --git a/agent_store/src/in_memory.rs b/agent_store/src/in_memory.rs index 7b016c66..1d2f566e 100644 --- a/agent_store/src/in_memory.rs +++ b/agent_store/src/in_memory.rs @@ -1,3 +1,4 @@ +use agent_holder::{services::HolderServices, state::HolderState}; use agent_issuance::{ offer::{ aggregate::Offer, @@ -7,7 +8,7 @@ use agent_issuance::{ }, }, services::IssuanceServices, - state::{CommandHandlers, IssuanceState, ViewRepositories}, + state::{IssuanceState, ViewRepositories}, SimpleLoggingQuery, }; use agent_shared::{application_state::Command, generic_query::generic_query}; @@ -135,7 +136,7 @@ pub async fn issuance_state( partition_event_publishers(event_publishers); IssuanceState { - command: CommandHandlers { + command: agent_issuance::state::CommandHandlers { server_config: Arc::new( server_config_event_publishers.into_iter().fold( AggregateHandler::new(()) @@ -173,6 +174,40 @@ pub async fn issuance_state( } } +pub async fn holder_state( + holder_services: Arc, + event_publishers: Vec>, +) -> HolderState { + // Initialize the in-memory repositories. + let credential = Arc::new(MemRepository::default()); + let offer = Arc::new(MemRepository::default()); + + // Partition the event_publishers into the different aggregates. + let (_, credential_event_publishers, offer_event_publishers, _, _) = partition_event_publishers(event_publishers); + + HolderState { + command: agent_holder::state::CommandHandlers { + credential: Arc::new( + vec![].into_iter().fold( + AggregateHandler::new(holder_services.clone()) + .append_query(SimpleLoggingQuery {}) + .append_query(generic_query(credential.clone())), + |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), + ), + ), + offer: Arc::new( + vec![].into_iter().fold( + AggregateHandler::new(holder_services.clone()) + .append_query(SimpleLoggingQuery {}) + .append_query(generic_query(offer.clone())), + |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), + ), + ), + }, + query: agent_holder::state::ViewRepositories { credential, offer }, + } +} + pub async fn verification_state( verification_services: Arc, event_publishers: Vec>, diff --git a/agent_store/src/postgres.rs b/agent_store/src/postgres.rs index 4d38d448..750c800a 100644 --- a/agent_store/src/postgres.rs +++ b/agent_store/src/postgres.rs @@ -1,3 +1,4 @@ +use agent_holder::{services::HolderServices, state::HolderState}; use agent_issuance::{ offer::queries::{access_token::AccessTokenQuery, pre_authorized_code::PreAuthorizedCodeQuery}, services::IssuanceServices, @@ -126,6 +127,46 @@ pub async fn issuance_state( } } +pub async fn holder_state( + holder_services: Arc, + event_publishers: Vec>, +) -> HolderState { + let connection_string = config().event_store.connection_string.clone().expect( + "Missing config parameter `event_store.connection_string` or `UNICORE__EVENT_STORE__CONNECTION_STRING`", + ); + let pool = default_postgress_pool(&connection_string).await; + + // Initialize the in-memory repositories. + let credential: Arc> = + Arc::new(PostgresViewRepository::new("holder_credential", pool.clone())); + let offer = Arc::new(PostgresViewRepository::new("received_offer", pool.clone())); + + // Partition the event_publishers into the different aggregates. + let (_, credential_event_publishers, offer_event_publishers, _, _) = partition_event_publishers(event_publishers); + + HolderState { + command: agent_holder::state::CommandHandlers { + credential: Arc::new( + vec![].into_iter().fold( + AggregateHandler::new(pool.clone(), holder_services.clone()) + .append_query(SimpleLoggingQuery {}) + .append_query(generic_query(credential.clone())), + |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), + ), + ), + offer: Arc::new( + vec![].into_iter().fold( + AggregateHandler::new(pool, holder_services.clone()) + .append_query(SimpleLoggingQuery {}) + .append_query(generic_query(offer.clone())), + |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), + ), + ), + }, + query: agent_holder::state::ViewRepositories { credential, offer }, + } +} + pub async fn verification_state( verification_services: Arc, event_publishers: Vec>, From 75d7a31f1215d8c800041f8ccc7c87ba3dd4401a Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Mon, 26 Aug 2024 10:11:30 +0200 Subject: [PATCH 04/31] feat: add Holder functionality to Event Publisher --- agent_event_publisher_http/Cargo.toml | 1 + agent_event_publisher_http/src/lib.rs | 58 ++++++++++++++++++++++----- agent_shared/src/config.rs | 14 +++++++ agent_store/src/in_memory.rs | 11 ++--- agent_store/src/lib.rs | 28 +++++++++++-- agent_store/src/postgres.rs | 11 ++--- 6 files changed, 99 insertions(+), 24 deletions(-) diff --git a/agent_event_publisher_http/Cargo.toml b/agent_event_publisher_http/Cargo.toml index a7e4f590..c2b7438c 100644 --- a/agent_event_publisher_http/Cargo.toml +++ b/agent_event_publisher_http/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true rust-version.workspace = true [dependencies] +agent_holder = { path = "../agent_holder" } agent_issuance = { path = "../agent_issuance" } agent_shared = { path = "../agent_shared" } agent_store = { path = "../agent_store" } diff --git a/agent_event_publisher_http/src/lib.rs b/agent_event_publisher_http/src/lib.rs index a8543298..e18be595 100644 --- a/agent_event_publisher_http/src/lib.rs +++ b/agent_event_publisher_http/src/lib.rs @@ -4,7 +4,7 @@ use agent_issuance::{ use agent_shared::config::config; use agent_store::{ AuthorizationRequestEventPublisher, ConnectionEventPublisher, CredentialEventPublisher, EventPublisher, - OfferEventPublisher, ServerConfigEventPublisher, + HolderCredentialEventPublisher, OfferEventPublisher, ReceivedOfferEventPublisher, ServerConfigEventPublisher, }; use agent_verification::{authorization_request::aggregate::AuthorizationRequest, connection::aggregate::Connection}; use async_trait::async_trait; @@ -15,13 +15,17 @@ use tracing::info; /// A struct that contains all the event publishers for the different aggregates. #[skip_serializing_none] -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Default)] pub struct EventPublisherHttp { // Issuance pub server_config: Option>, pub credential: Option>, pub offer: Option>, + // Holder + pub holder_credential: Option>, + pub received_offer: Option>, + // Verification pub connection: Option>, pub authorization_request: Option>, @@ -33,13 +37,7 @@ impl EventPublisherHttp { // If it's not enabled, return an empty event publisher. if !event_publisher_http.enabled { - return Ok(EventPublisherHttp { - server_config: None, - credential: None, - offer: None, - connection: None, - authorization_request: None, - }); + return Ok(EventPublisherHttp::default()); } let server_config = (!event_publisher_http.events.server_config.is_empty()).then(|| { @@ -54,12 +52,12 @@ impl EventPublisherHttp { ) }); - let credential = (!event_publisher_http.events.offer.is_empty()).then(|| { + let credential = (!event_publisher_http.events.credential.is_empty()).then(|| { AggregateEventPublisherHttp::::new( event_publisher_http.target_url.clone(), event_publisher_http .events - .offer + .credential .iter() .map(ToString::to_string) .collect(), @@ -78,6 +76,30 @@ impl EventPublisherHttp { ) }); + let holder_credential = (!event_publisher_http.events.holder_credential.is_empty()).then(|| { + AggregateEventPublisherHttp::::new( + event_publisher_http.target_url.clone(), + event_publisher_http + .events + .holder_credential + .iter() + .map(ToString::to_string) + .collect(), + ) + }); + + let received_offer = (!event_publisher_http.events.received_offer.is_empty()).then(|| { + AggregateEventPublisherHttp::::new( + event_publisher_http.target_url.clone(), + event_publisher_http + .events + .received_offer + .iter() + .map(ToString::to_string) + .collect(), + ) + }); + let connection = (!event_publisher_http.events.connection.is_empty()).then(|| { AggregateEventPublisherHttp::::new( event_publisher_http.target_url.clone(), @@ -106,6 +128,8 @@ impl EventPublisherHttp { server_config, credential, offer, + holder_credential, + received_offer, connection, authorization_request, }; @@ -135,6 +159,18 @@ impl EventPublisher for EventPublisherHttp { .map(|publisher| Box::new(publisher) as OfferEventPublisher) } + fn holder_credential(&mut self) -> Option { + self.holder_credential + .take() + .map(|publisher| Box::new(publisher) as HolderCredentialEventPublisher) + } + + fn received_offer(&mut self) -> Option { + self.received_offer + .take() + .map(|publisher| Box::new(publisher) as ReceivedOfferEventPublisher) + } + fn connection(&mut self) -> Option { self.connection .take() diff --git a/agent_shared/src/config.rs b/agent_shared/src/config.rs index d1eb7db5..2436f87a 100644 --- a/agent_shared/src/config.rs +++ b/agent_shared/src/config.rs @@ -114,6 +114,10 @@ pub struct Events { #[serde(default)] pub offer: Vec, #[serde(default)] + pub holder_credential: Vec, + #[serde(default)] + pub received_offer: Vec, + #[serde(default)] pub connection: Vec, #[serde(default)] pub authorization_request: Vec, @@ -142,6 +146,16 @@ pub enum OfferEvent { CredentialResponseCreated, } +#[derive(Debug, Serialize, Deserialize, Clone, strum::Display)] +pub enum HolderCredentialEvent { + // FIX THIS +} + +#[derive(Debug, Serialize, Deserialize, Clone, strum::Display)] +pub enum ReceivedOfferEvent { + // FIX THIS +} + #[derive(Debug, Serialize, Deserialize, Clone, strum::Display)] pub enum ConnectionEvent { SIOPv2AuthorizationResponseVerified, diff --git a/agent_store/src/in_memory.rs b/agent_store/src/in_memory.rs index 1d2f566e..2eb90f46 100644 --- a/agent_store/src/in_memory.rs +++ b/agent_store/src/in_memory.rs @@ -132,7 +132,7 @@ pub async fn issuance_state( let access_token_query = AccessTokenQuery::new(access_token.clone()); // Partition the event_publishers into the different aggregates. - let (server_config_event_publishers, credential_event_publishers, offer_event_publishers, _, _) = + let (server_config_event_publishers, credential_event_publishers, offer_event_publishers, _, _, _, _) = partition_event_publishers(event_publishers); IssuanceState { @@ -183,12 +183,13 @@ pub async fn holder_state( let offer = Arc::new(MemRepository::default()); // Partition the event_publishers into the different aggregates. - let (_, credential_event_publishers, offer_event_publishers, _, _) = partition_event_publishers(event_publishers); + let (_, _, _, credential_event_publishers, offer_event_publishers, _, _) = + partition_event_publishers(event_publishers); HolderState { command: agent_holder::state::CommandHandlers { credential: Arc::new( - vec![].into_iter().fold( + credential_event_publishers.into_iter().fold( AggregateHandler::new(holder_services.clone()) .append_query(SimpleLoggingQuery {}) .append_query(generic_query(credential.clone())), @@ -196,7 +197,7 @@ pub async fn holder_state( ), ), offer: Arc::new( - vec![].into_iter().fold( + offer_event_publishers.into_iter().fold( AggregateHandler::new(holder_services.clone()) .append_query(SimpleLoggingQuery {}) .append_query(generic_query(offer.clone())), @@ -217,7 +218,7 @@ pub async fn verification_state( let connection = Arc::new(MemRepository::default()); // Partition the event_publishers into the different aggregates. - let (_, _, _, authorization_request_event_publishers, connection_event_publishers) = + let (_, _, _, _, _, authorization_request_event_publishers, connection_event_publishers) = partition_event_publishers(event_publishers); VerificationState { diff --git a/agent_store/src/lib.rs b/agent_store/src/lib.rs index f40f6623..b5baaf80 100644 --- a/agent_store/src/lib.rs +++ b/agent_store/src/lib.rs @@ -10,6 +10,8 @@ pub mod postgres; pub type ServerConfigEventPublisher = Box>; pub type CredentialEventPublisher = Box>; pub type OfferEventPublisher = Box>; +pub type HolderCredentialEventPublisher = Box>; +pub type ReceivedOfferEventPublisher = Box>; pub type AuthorizationRequestEventPublisher = Box>; pub type ConnectionEventPublisher = Box>; @@ -18,6 +20,8 @@ pub type Partitions = ( Vec, Vec, Vec, + Vec, + Vec, Vec, Vec, ); @@ -37,6 +41,13 @@ pub trait EventPublisher { None } + fn holder_credential(&mut self) -> Option { + None + } + fn received_offer(&mut self) -> Option { + None + } + fn connection(&mut self) -> Option { None } @@ -47,7 +58,7 @@ pub trait EventPublisher { pub(crate) fn partition_event_publishers(event_publishers: Vec>) -> Partitions { event_publishers.into_iter().fold( - (vec![], vec![], vec![], vec![], vec![]), + (vec![], vec![], vec![], vec![], vec![], vec![], vec![]), |mut partitions, mut event_publisher| { if let Some(server_config) = event_publisher.server_config() { partitions.0.push(server_config); @@ -59,11 +70,18 @@ pub(crate) fn partition_event_publishers(event_publishers: Vec Date: Mon, 26 Aug 2024 11:45:20 +0200 Subject: [PATCH 05/31] feat: add `SendCredentialOffer` to `agent_verification` --- agent_event_publisher_http/Cargo.toml | 2 +- agent_issuance/Cargo.toml | 1 + agent_issuance/src/offer/aggregate.rs | 89 +++++++++++++++++++------ agent_issuance/src/offer/command.rs | 7 +- agent_issuance/src/offer/event.rs | 11 ++- agent_issuance/src/offer/queries/mod.rs | 6 +- 6 files changed, 90 insertions(+), 26 deletions(-) diff --git a/agent_event_publisher_http/Cargo.toml b/agent_event_publisher_http/Cargo.toml index c2b7438c..a9a0f29d 100644 --- a/agent_event_publisher_http/Cargo.toml +++ b/agent_event_publisher_http/Cargo.toml @@ -19,7 +19,7 @@ rustls = { version = "0.23", default-features = false, features = [ "std", "tls12" ] } -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +reqwest.workspace = true serde.workspace = true serde_with.workspace = true serde_yaml.workspace = true diff --git a/agent_issuance/Cargo.toml b/agent_issuance/Cargo.toml index 37eeb16c..247df085 100644 --- a/agent_issuance/Cargo.toml +++ b/agent_issuance/Cargo.toml @@ -22,6 +22,7 @@ jsonwebtoken.workspace = true oid4vci.workspace = true oid4vc-core.workspace = true oid4vc-manager.workspace = true +reqwest.workspace = true serde.workspace = true serde_json.workspace = true thiserror.workspace = true diff --git a/agent_issuance/src/offer/aggregate.rs b/agent_issuance/src/offer/aggregate.rs index ac669a82..e511004a 100644 --- a/agent_issuance/src/offer/aggregate.rs +++ b/agent_issuance/src/offer/aggregate.rs @@ -18,6 +18,7 @@ use crate::services::IssuanceServices; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Offer { + pub credential_offer: Option, pub subject_id: Option, pub credential_ids: Vec, pub form_url_encoded_credential_offer: String, @@ -45,7 +46,10 @@ impl Aggregate for Offer { info!("Handling command: {:?}", command); match command { - CreateCredentialOffer { offer_id } => { + CreateCredentialOffer { + offer_id, + credential_issuer_metadata, + } => { #[cfg(test)] let (pre_authorized_code, access_token) = { let pre_authorized_code = tests::PRE_AUTHORIZED_CODES.lock().unwrap().pop_front().unwrap(); @@ -55,23 +59,6 @@ impl Aggregate for Offer { #[cfg(not(test))] let (pre_authorized_code, access_token) = { (generate_random_string(), generate_random_string()) }; - Ok(vec![CredentialOfferCreated { - offer_id, - pre_authorized_code, - access_token, - }]) - } - AddCredentials { - offer_id, - credential_ids, - } => Ok(vec![CredentialsAdded { - offer_id, - credential_ids, - }]), - CreateFormUrlEncodedCredentialOffer { - offer_id, - credential_issuer_metadata, - } => { // TODO: This needs to be fixed when we implement Batch credentials. let credentials_supported = credential_issuer_metadata.credential_configurations_supported.clone(); let credential_offer = CredentialOffer::CredentialOffer(Box::new(CredentialOfferParameters { @@ -80,16 +67,47 @@ impl Aggregate for Offer { grants: Some(Grants { authorization_code: None, pre_authorized_code: Some(PreAuthorizedCode { - pre_authorized_code: self.pre_authorized_code.clone(), + pre_authorized_code: pre_authorized_code.clone(), ..Default::default() }), }), })); - Ok(vec![FormUrlEncodedCredentialOfferCreated { + + Ok(vec![CredentialOfferCreated { offer_id, - form_url_encoded_credential_offer: credential_offer.to_string(), + credential_offer, + pre_authorized_code, + access_token, }]) } + AddCredentials { + offer_id, + credential_ids, + } => Ok(vec![CredentialsAdded { + offer_id, + credential_ids, + }]), + CreateFormUrlEncodedCredentialOffer { offer_id } => Ok(vec![FormUrlEncodedCredentialOfferCreated { + offer_id, + form_url_encoded_credential_offer: self.credential_offer.as_ref().unwrap().to_string(), + }]), + SendCredentialOffer { offer_id, target_url } => { + let client = reqwest::Client::new(); + + let response = client + .get(target_url.clone()) + .header("Content-Type", "application/x-www-form-urlencoded") + .json(self.credential_offer.as_ref().unwrap()) + .send() + .await + .unwrap(); + + if response.status().is_success() { + Ok(vec![CredentialOfferSent { offer_id, target_url }]) + } else { + todo!() + } + } CreateTokenResponse { offer_id, token_request, @@ -180,10 +198,12 @@ impl Aggregate for Offer { CredentialOfferCreated { pre_authorized_code, access_token, + credential_offer, .. } => { self.pre_authorized_code = pre_authorized_code; self.access_token = access_token; + self.credential_offer.replace(credential_offer); } CredentialsAdded { credential_ids, .. } => { self.credential_ids = credential_ids; @@ -194,6 +214,7 @@ impl Aggregate for Offer { } => { self.form_url_encoded_credential_offer = form_url_encoded_credential_offer; } + CredentialOfferSent { .. } => {} CredentialRequestVerified { subject_id, .. } => { self.subject_id.replace(subject_id); } @@ -247,9 +268,11 @@ pub mod tests { .given_no_previous_events() .when(OfferCommand::CreateCredentialOffer { offer_id: Default::default(), + credential_issuer_metadata: CREDENTIAL_ISSUER_METADATA.clone(), }) .then_expect_events(vec![OfferEvent::CredentialOfferCreated { offer_id: Default::default(), + credential_offer: subject.credential_offer.clone(), pre_authorized_code: subject.pre_authorized_code, access_token: subject.access_token, }]); @@ -266,6 +289,7 @@ pub mod tests { OfferTestFramework::with(test_issuance_services()) .given(vec![OfferEvent::CredentialOfferCreated { offer_id: Default::default(), + credential_offer: subject.credential_offer.clone(), pre_authorized_code: subject.pre_authorized_code.clone(), access_token: subject.access_token.clone(), }]) @@ -291,6 +315,7 @@ pub mod tests { .given(vec![ OfferEvent::CredentialOfferCreated { offer_id: Default::default(), + credential_offer: subject.credential_offer.clone(), pre_authorized_code: subject.pre_authorized_code, access_token: subject.access_token, }, @@ -301,7 +326,6 @@ pub mod tests { ]) .when(OfferCommand::CreateFormUrlEncodedCredentialOffer { offer_id: Default::default(), - credential_issuer_metadata: CREDENTIAL_ISSUER_METADATA.clone(), }) .then_expect_events(vec![OfferEvent::FormUrlEncodedCredentialOfferCreated { offer_id: Default::default(), @@ -321,6 +345,7 @@ pub mod tests { .given(vec![ OfferEvent::CredentialOfferCreated { offer_id: Default::default(), + credential_offer: subject.credential_offer.clone(), pre_authorized_code: subject.pre_authorized_code.clone(), access_token: subject.access_token.clone(), }, @@ -355,6 +380,7 @@ pub mod tests { .given(vec![ OfferEvent::CredentialOfferCreated { offer_id: Default::default(), + credential_offer: subject.credential_offer.clone(), pre_authorized_code: subject.pre_authorized_code.clone(), access_token: subject.access_token.clone(), }, @@ -395,6 +421,7 @@ pub mod tests { .given(vec![ OfferEvent::CredentialOfferCreated { offer_id: Default::default(), + credential_offer: subject.credential_offer.clone(), pre_authorized_code: subject.pre_authorized_code.clone(), access_token: subject.access_token.clone(), }, @@ -428,6 +455,7 @@ pub mod tests { #[derive(Clone)] struct TestSubject { subject: Arc, + credential_offer: CredentialOffer, credential: String, access_token: String, pre_authorized_code: String, @@ -445,9 +473,26 @@ pub mod tests { fn test_subject() -> TestSubject { let pre_authorized_code = PRE_AUTHORIZED_CODES.lock().unwrap()[0].clone(); + let credential_offer = CredentialOffer::CredentialOffer(Box::new(CredentialOfferParameters { + credential_issuer: CREDENTIAL_ISSUER_METADATA.credential_issuer.clone(), + credential_configuration_ids: CREDENTIAL_ISSUER_METADATA + .credential_configurations_supported + .keys() + .cloned() + .collect(), + grants: Some(Grants { + authorization_code: None, + pre_authorized_code: Some(PreAuthorizedCode { + pre_authorized_code: pre_authorized_code.clone(), + ..Default::default() + }), + }), + })); + TestSubject { subject: SUBJECT_KEY_DID.clone(), credential: OPENBADGE_VERIFIABLE_CREDENTIAL_JWT.to_string(), + credential_offer, pre_authorized_code: pre_authorized_code.clone(), access_token: ACCESS_TOKENS.lock().unwrap()[0].clone(), form_url_encoded_credential_offer: format!("openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fexample.com%2F%22%2C%22credential_configuration_ids%22%3A%5B%220%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22{pre_authorized_code}%22%7D%7D%7D"), diff --git a/agent_issuance/src/offer/command.rs b/agent_issuance/src/offer/command.rs index 8e4e8fc2..ad58d100 100644 --- a/agent_issuance/src/offer/command.rs +++ b/agent_issuance/src/offer/command.rs @@ -7,23 +7,28 @@ use oid4vci::{ token_request::TokenRequest, }; use serde::Deserialize; +use url::Url; #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum OfferCommand { CreateCredentialOffer { offer_id: String, + credential_issuer_metadata: CredentialIssuerMetadata, }, AddCredentials { offer_id: String, credential_ids: Vec, }, + SendCredentialOffer { + offer_id: String, + target_url: Url, + }, // OpenID4VCI Pre-Authorized Code Flow // TODO: add option for credential_offer_uri (by reference) CreateFormUrlEncodedCredentialOffer { offer_id: String, - credential_issuer_metadata: CredentialIssuerMetadata, }, CreateTokenResponse { offer_id: String, diff --git a/agent_issuance/src/offer/event.rs b/agent_issuance/src/offer/event.rs index 9fd2d03c..6e3fb1bf 100644 --- a/agent_issuance/src/offer/event.rs +++ b/agent_issuance/src/offer/event.rs @@ -1,11 +1,15 @@ use cqrs_es::DomainEvent; -use oid4vci::{credential_response::CredentialResponse, token_response::TokenResponse}; +use oid4vci::{ + credential_offer::CredentialOffer, credential_response::CredentialResponse, token_response::TokenResponse, +}; use serde::{Deserialize, Serialize}; +use url::Url; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum OfferEvent { CredentialOfferCreated { offer_id: String, + credential_offer: CredentialOffer, pre_authorized_code: String, access_token: String, }, @@ -17,6 +21,10 @@ pub enum OfferEvent { offer_id: String, form_url_encoded_credential_offer: String, }, + CredentialOfferSent { + offer_id: String, + target_url: Url, + }, TokenResponseCreated { offer_id: String, token_response: TokenResponse, @@ -39,6 +47,7 @@ impl DomainEvent for OfferEvent { CredentialOfferCreated { .. } => "CredentialOfferCreated", CredentialsAdded { .. } => "CredentialsAdded", FormUrlEncodedCredentialOfferCreated { .. } => "FormUrlEncodedCredentialOfferCreated", + CredentialOfferSent { .. } => "CredentialOfferSent", TokenResponseCreated { .. } => "TokenResponseCreated", CredentialRequestVerified { .. } => "CredentialRequestVerified", CredentialResponseCreated { .. } => "CredentialResponseCreated", diff --git a/agent_issuance/src/offer/queries/mod.rs b/agent_issuance/src/offer/queries/mod.rs index 746447b4..24be5166 100644 --- a/agent_issuance/src/offer/queries/mod.rs +++ b/agent_issuance/src/offer/queries/mod.rs @@ -6,7 +6,9 @@ use cqrs_es::{ persist::{PersistenceError, ViewContext, ViewRepository}, EventEnvelope, Query, View, }; -use oid4vci::{credential_response::CredentialResponse, token_response::TokenResponse}; +use oid4vci::{ + credential_offer::CredentialOffer, credential_response::CredentialResponse, token_response::TokenResponse, +}; use serde::{Deserialize, Serialize}; use crate::offer::aggregate::Offer; @@ -28,6 +30,7 @@ where #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct OfferView { + pub credential_offer: Option, pub subject_id: Option, pub credential_ids: Vec, pub pre_authorized_code: String, @@ -62,6 +65,7 @@ impl View for OfferView { } => self .form_url_encoded_credential_offer .clone_from(form_url_encoded_credential_offer), + CredentialOfferSent { .. } => {} CredentialRequestVerified { subject_id, .. } => { self.subject_id.replace(subject_id.clone()); } From 87d061fcbb70170a3f837df47bcbc4dc67310617 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Mon, 26 Aug 2024 14:25:41 +0200 Subject: [PATCH 06/31] feat: add `/offers/send` issuance endpoint to `agent_api_rest` --- Cargo.lock | 2 + Cargo.toml | 1 + agent_api_rest/Cargo.toml | 2 +- agent_api_rest/src/issuance/credentials.rs | 10 +++++ .../src/issuance/{offers.rs => offers/mod.rs} | 22 +++++----- agent_api_rest/src/issuance/offers/send.rs | 40 +++++++++++++++++++ agent_api_rest/src/lib.rs | 23 ++++++----- 7 files changed, 80 insertions(+), 20 deletions(-) rename agent_api_rest/src/issuance/{offers.rs => offers/mod.rs} (98%) create mode 100644 agent_api_rest/src/issuance/offers/send.rs diff --git a/Cargo.lock b/Cargo.lock index bc6e9c05..f9ead83a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,7 @@ name = "agent_event_publisher_http" version = "0.1.0" dependencies = [ "agent_event_publisher_http", + "agent_holder", "agent_issuance", "agent_shared", "agent_store", @@ -197,6 +198,7 @@ dependencies = [ "oid4vc-core", "oid4vc-manager", "oid4vci", + "reqwest 0.12.5", "rstest", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 186bf2d5..6583dbd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ identity_credential = { version = "1.3", default-features = false, features = [ identity_iota = { version = "1.3" } jsonwebtoken = "9.3" lazy_static = "1.4" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } rstest = "0.19" serde = { version = "1.0", default-features = false, features = ["derive"] } serde_json = { version = "1.0" } diff --git a/agent_api_rest/Cargo.toml b/agent_api_rest/Cargo.toml index 48b80cab..ef84cc78 100644 --- a/agent_api_rest/Cargo.toml +++ b/agent_api_rest/Cargo.toml @@ -25,6 +25,7 @@ tokio.workspace = true tower-http.workspace = true tracing.workspace = true tracing-subscriber.workspace = true +url.workspace = true uuid.workspace = true [dev-dependencies] @@ -48,5 +49,4 @@ serde_yaml.workspace = true serial_test = "3.0" tower = { version = "0.4" } tracing-test.workspace = true -url.workspace = true wiremock.workspace = true diff --git a/agent_api_rest/src/issuance/credentials.rs b/agent_api_rest/src/issuance/credentials.rs index d2f4ca1c..6152a940 100644 --- a/agent_api_rest/src/issuance/credentials.rs +++ b/agent_api_rest/src/issuance/credentials.rs @@ -106,6 +106,15 @@ pub(crate) async fn credentials( return StatusCode::INTERNAL_SERVER_ERROR.into_response(); } + // Get the `CredentialIssuerMetadata` from the `ServerConfigView`. + let credential_issuer_metadata = match query_handler(SERVER_CONFIG_ID, &state.query.server_config).await { + Ok(Some(ServerConfigView { + credential_issuer_metadata: Some(credential_issuer_metadata), + .. + })) => credential_issuer_metadata, + _ => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), + }; + // Create an offer if it does not exist yet. match query_handler(&offer_id, &state.query.offer).await { Ok(Some(_)) => {} @@ -115,6 +124,7 @@ pub(crate) async fn credentials( &state.command.offer, OfferCommand::CreateCredentialOffer { offer_id: offer_id.clone(), + credential_issuer_metadata, }, ) .await diff --git a/agent_api_rest/src/issuance/offers.rs b/agent_api_rest/src/issuance/offers/mod.rs similarity index 98% rename from agent_api_rest/src/issuance/offers.rs rename to agent_api_rest/src/issuance/offers/mod.rs index fa78ea2a..d5a7fdfe 100644 --- a/agent_api_rest/src/issuance/offers.rs +++ b/agent_api_rest/src/issuance/offers/mod.rs @@ -1,3 +1,5 @@ +pub mod send; + use agent_issuance::{ offer::{command::OfferCommand, queries::OfferView}, server_config::queries::ServerConfigView, @@ -28,6 +30,15 @@ pub(crate) async fn offers(State(state): State, Json(payload): Js return (StatusCode::BAD_REQUEST, "invalid payload").into_response(); }; + // Get the `CredentialIssuerMetadata` from the `ServerConfigView`. + let credential_issuer_metadata = match query_handler(SERVER_CONFIG_ID, &state.query.server_config).await { + Ok(Some(ServerConfigView { + credential_issuer_metadata: Some(credential_issuer_metadata), + .. + })) => credential_issuer_metadata, + _ => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), + }; + // Create an offer if it does not exist yet. match query_handler(&offer_id, &state.query.offer).await { Ok(Some(_)) => {} @@ -37,6 +48,7 @@ pub(crate) async fn offers(State(state): State, Json(payload): Js &state.command.offer, OfferCommand::CreateCredentialOffer { offer_id: offer_id.clone(), + credential_issuer_metadata, }, ) .await @@ -47,18 +59,8 @@ pub(crate) async fn offers(State(state): State, Json(payload): Js } }; - // Get the `CredentialIssuerMetadata` from the `ServerConfigView`. - let credential_issuer_metadata = match query_handler(SERVER_CONFIG_ID, &state.query.server_config).await { - Ok(Some(ServerConfigView { - credential_issuer_metadata: Some(credential_issuer_metadata), - .. - })) => credential_issuer_metadata, - _ => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), - }; - let command = OfferCommand::CreateFormUrlEncodedCredentialOffer { offer_id: offer_id.clone(), - credential_issuer_metadata, }; if command_handler(&offer_id, &state.command.offer, command).await.is_err() { diff --git a/agent_api_rest/src/issuance/offers/send.rs b/agent_api_rest/src/issuance/offers/send.rs new file mode 100644 index 00000000..2e9a973a --- /dev/null +++ b/agent_api_rest/src/issuance/offers/send.rs @@ -0,0 +1,40 @@ +use agent_issuance::{offer::command::OfferCommand, state::IssuanceState}; +use agent_shared::handlers::command_handler; +use axum::{ + extract::State, + response::{IntoResponse, Response}, + Json, +}; +use hyper::StatusCode; +use serde::{Deserialize, Serialize}; +use tracing::info; +use url::Url; + +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SendOfferEndpointRequest { + pub offer_id: String, + pub target_url: Url, +} + +#[axum_macros::debug_handler] +pub(crate) async fn send(State(state): State, Json(payload): Json) -> Response { + info!("Request Body: {}", payload); + + let Ok(SendOfferEndpointRequest { offer_id, target_url }) = serde_json::from_value(payload) else { + return (StatusCode::BAD_REQUEST, "invalid payload").into_response(); + }; + + let command = OfferCommand::SendCredentialOffer { + offer_id: offer_id.clone(), + target_url, + }; + + // Send the Credential Offer to the `target_url`. + match command_handler(&offer_id, &state.command.offer, command).await { + Ok(_) => StatusCode::OK.into_response(), + // TODO: add better Error responses. This needs to be done properly in all endpoints once + // https://github.com/impierce/openid4vc/issues/78 is fixed. + Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } +} diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index d18beebf..7fcaccbc 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -14,15 +14,18 @@ use axum::{ routing::{get, post}, Router, }; -use issuance::credential_issuer::{ - credential::credential, - token::token, - well_known::{ - oauth_authorization_server::oauth_authorization_server, openid_credential_issuer::openid_credential_issuer, - }, -}; use issuance::credentials::{credentials, get_credentials}; use issuance::offers::offers; +use issuance::{ + credential_issuer::{ + credential::credential, + token::token, + well_known::{ + oauth_authorization_server::oauth_authorization_server, openid_credential_issuer::openid_credential_issuer, + }, + }, + offers::send::send, +}; use tower_http::trace::TraceLayer; use tracing::{info_span, Span}; use verification::{ @@ -45,15 +48,17 @@ pub fn app(state: ApplicationState) -> Router { } }; + // TODO: refactor routes into a nice and consistant folder structure. Router::new() .nest( &path(API_VERSION), Router::new() - // Agent Issuance Preparations + // Agent Issuance .route("/credentials", post(credentials)) .route("/credentials/:credential_id", get(get_credentials)) .route("/offers", post(offers)) - // Agent Verification Preparations + .route("/offers/send", post(send)) + // Agent Verification .route("/authorization_requests", post(authorization_requests)) .route( "/authorization_requests/:authorization_request_id", From 34ff2809eb9bc205611302901eb038dbccbf7b1d Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Tue, 27 Aug 2024 09:56:34 +0200 Subject: [PATCH 07/31] fix: remove incorrect Content Type --- agent_issuance/src/offer/aggregate.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/agent_issuance/src/offer/aggregate.rs b/agent_issuance/src/offer/aggregate.rs index e511004a..9f00a530 100644 --- a/agent_issuance/src/offer/aggregate.rs +++ b/agent_issuance/src/offer/aggregate.rs @@ -96,7 +96,6 @@ impl Aggregate for Offer { let response = client .get(target_url.clone()) - .header("Content-Type", "application/x-www-form-urlencoded") .json(self.credential_offer.as_ref().unwrap()) .send() .await From 7f1ab4f8de05ba376a37467e0a3bcc68eefe1ce8 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Tue, 27 Aug 2024 09:57:40 +0200 Subject: [PATCH 08/31] feat: add `Status` enum --- agent_holder/src/offer/aggregate.rs | 44 +++++++++++++++++++++++++---- agent_holder/src/offer/event.rs | 5 ++++ agent_holder/src/offer/queries.rs | 3 +- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/agent_holder/src/offer/aggregate.rs b/agent_holder/src/offer/aggregate.rs index c9d291de..9efe353a 100644 --- a/agent_holder/src/offer/aggregate.rs +++ b/agent_holder/src/offer/aggregate.rs @@ -19,11 +19,24 @@ use crate::offer::error::OfferError::{self, *}; use crate::offer::event::OfferEvent; use crate::services::HolderServices; +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] +pub enum Status { + #[default] + Pending, + Accepted, + Received, + Rejected, +} + #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Offer { pub credential_offer: Option, + pub status: Status, pub credential_configurations: Option>, pub token_response: Option, + // TODO: These should not be part of this Aggregate. Instead, an Event Subscriber should be listening to the + // `CredentialResponseReceived` event and then trigger the `CredentialCommand::AddCredential` command. We can do + // this once we have a mechanism implemented that can both listen to events as well as trigger commands. pub credentials: Vec, // pub subject_id: Option, // pub credential_ids: Vec, @@ -90,7 +103,10 @@ impl Aggregate for Offer { credential_configurations, }]) } - AcceptCredentialOffer { offer_id } => Ok(vec![CredentialOfferAccepted { offer_id }]), + AcceptCredentialOffer { offer_id } => Ok(vec![CredentialOfferAccepted { + offer_id, + status: Status::Accepted, + }]), SendTokenRequest { offer_id } => { let wallet = &services.wallet; @@ -181,9 +197,16 @@ impl Aggregate for Offer { info!("credentials: {:?}", credentials); - Ok(vec![CredentialResponseReceived { offer_id, credentials }]) + Ok(vec![CredentialResponseReceived { + offer_id, + status: Status::Received, + credentials, + }]) } - RejectCredentialOffer { offer_id } => todo!(), + RejectCredentialOffer { offer_id } => Ok(vec![CredentialOfferRejected { + offer_id, + status: Status::Rejected, + }]), } } @@ -193,8 +216,13 @@ impl Aggregate for Offer { info!("Applying event: {:?}", event); match event { - CredentialOfferReceived { credential_offer, .. } => { + CredentialOfferReceived { + credential_offer, + credential_configurations, + .. + } => { self.credential_offer.replace(credential_offer); + self.credential_configurations.replace(credential_configurations); } TokenResponseReceived { token_response, .. } => { self.token_response.replace(token_response); @@ -202,8 +230,12 @@ impl Aggregate for Offer { CredentialResponseReceived { credentials, .. } => { self.credentials = credentials; } - CredentialOfferAccepted { .. } => {} - CredentialOfferRejected { .. } => {} + CredentialOfferAccepted { status, .. } => { + self.status = status; + } + CredentialOfferRejected { status, .. } => { + self.status = status; + } } } } diff --git a/agent_holder/src/offer/event.rs b/agent_holder/src/offer/event.rs index dcca3304..8df88a82 100644 --- a/agent_holder/src/offer/event.rs +++ b/agent_holder/src/offer/event.rs @@ -7,6 +7,8 @@ use oid4vci::{ }; use serde::{Deserialize, Serialize}; +use super::aggregate::Status; + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum OfferEvent { CredentialOfferReceived { @@ -16,6 +18,7 @@ pub enum OfferEvent { }, CredentialOfferAccepted { offer_id: String, + status: Status, }, TokenResponseReceived { offer_id: String, @@ -23,10 +26,12 @@ pub enum OfferEvent { }, CredentialResponseReceived { offer_id: String, + status: Status, credentials: Vec, }, CredentialOfferRejected { offer_id: String, + status: Status, }, } diff --git a/agent_holder/src/offer/queries.rs b/agent_holder/src/offer/queries.rs index 35180e30..43255ffa 100644 --- a/agent_holder/src/offer/queries.rs +++ b/agent_holder/src/offer/queries.rs @@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize}; use crate::offer::aggregate::Offer; -use super::event::OfferEvent; +use super::{aggregate::Status, event::OfferEvent}; /// A custom query trait for the Offer aggregate. This trait is used to define custom queries for the Offer aggregate /// that do not make use of `GenericQuery`. @@ -35,6 +35,7 @@ where #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct OfferView { pub credential_offer: Option, + pub status: Status, pub credential_configurations: Option>, pub token_response: Option, pub credentials: Vec, From 302e63f64ff9439d0369c2ffe497a39ee914e161 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Tue, 27 Aug 2024 09:58:09 +0200 Subject: [PATCH 09/31] feat: add REST API for Holder --- agent_api_rest/src/holder/holder/mod.rs | 1 + .../src/holder/holder/offers/accept.rs | 78 +++++++++++++++++++ .../src/holder/holder/offers/mod.rs | 23 ++++++ .../src/holder/holder/offers/reject.rs | 24 ++++++ agent_api_rest/src/holder/mod.rs | 5 ++ agent_api_rest/src/holder/openid4vci/mod.rs | 44 +++++++++++ agent_api_rest/src/lib.rs | 11 +++ 7 files changed, 186 insertions(+) create mode 100644 agent_api_rest/src/holder/holder/mod.rs create mode 100644 agent_api_rest/src/holder/holder/offers/accept.rs create mode 100644 agent_api_rest/src/holder/holder/offers/mod.rs create mode 100644 agent_api_rest/src/holder/holder/offers/reject.rs create mode 100644 agent_api_rest/src/holder/mod.rs create mode 100644 agent_api_rest/src/holder/openid4vci/mod.rs diff --git a/agent_api_rest/src/holder/holder/mod.rs b/agent_api_rest/src/holder/holder/mod.rs new file mode 100644 index 00000000..4791300f --- /dev/null +++ b/agent_api_rest/src/holder/holder/mod.rs @@ -0,0 +1 @@ +pub mod offers; diff --git a/agent_api_rest/src/holder/holder/offers/accept.rs b/agent_api_rest/src/holder/holder/offers/accept.rs new file mode 100644 index 00000000..f2626798 --- /dev/null +++ b/agent_api_rest/src/holder/holder/offers/accept.rs @@ -0,0 +1,78 @@ +use agent_holder::{ + credential::command::CredentialCommand, + offer::{command::OfferCommand, queries::OfferView}, + state::HolderState, +}; +use agent_shared::handlers::{command_handler, query_handler}; +use axum::{ + extract::{Path, State}, + response::{IntoResponse, Response}, +}; +use hyper::StatusCode; + +#[axum_macros::debug_handler] +pub(crate) async fn accept(State(state): State, Path(offer_id): Path) -> Response { + // TODO: General note that also applies to other endpoints. Currently we are using Application Layer logic in the + // REST API. This is not ideal and should be changed. The REST API should only be responsible for handling HTTP + // Requests and Responses. + // Furthermore, the to be implemented Application Layer should be kept very thin as well. See: https://github.com/impierce/ssi-agent/issues/114 + + let command = OfferCommand::AcceptCredentialOffer { + offer_id: offer_id.clone(), + }; + + // Add the Credential Offer to the state. + if command_handler(&offer_id, &state.command.offer, command).await.is_err() { + // TODO: add better Error responses. This needs to be done properly in all endpoints once + // https://github.com/impierce/openid4vc/issues/78 is fixed. + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + + let command = OfferCommand::SendTokenRequest { + offer_id: offer_id.clone(), + }; + + // Add the Credential Offer to the state. + if command_handler(&offer_id, &state.command.offer, command).await.is_err() { + // TODO: add better Error responses. This needs to be done properly in all endpoints once + // https://github.com/impierce/openid4vc/issues/78 is fixed. + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + + let command = OfferCommand::SendCredentialRequest { + offer_id: offer_id.clone(), + }; + + // Add the Credential Offer to the state. + if command_handler(&offer_id, &state.command.offer, command).await.is_err() { + // TODO: add better Error responses. This needs to be done properly in all endpoints once + // https://github.com/impierce/openid4vc/issues/78 is fixed. + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + + let credentials = match query_handler(&offer_id, &state.query.offer).await { + Ok(Some(OfferView { credentials, .. })) => credentials, + _ => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), + }; + + for credential in credentials { + let credential_id = uuid::Uuid::new_v4().to_string(); + + let command = CredentialCommand::AddCredential { + credential_id: credential_id.clone(), + offer_id: offer_id.clone(), + credential, + }; + + // Add the Credential to the state. + if command_handler(&credential_id, &state.command.credential, command) + .await + .is_err() + { + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + } + + // TODO: What do we return here? + StatusCode::OK.into_response() +} diff --git a/agent_api_rest/src/holder/holder/offers/mod.rs b/agent_api_rest/src/holder/holder/offers/mod.rs new file mode 100644 index 00000000..6237a28d --- /dev/null +++ b/agent_api_rest/src/holder/holder/offers/mod.rs @@ -0,0 +1,23 @@ +pub mod accept; +pub mod reject; + +use agent_holder::state::HolderState; +use agent_shared::handlers::query_handler; +use axum::{ + extract::State, + response::{IntoResponse, Response}, + Json, +}; +use hyper::StatusCode; + +use crate::holder::TEMP_OFFER_ID; + +#[axum_macros::debug_handler] +pub(crate) async fn offers(State(state): State) -> Response { + // TODO: Add extension that allows for selecting all offers. + match query_handler(TEMP_OFFER_ID, &state.query.offer).await { + Ok(Some(offer_view)) => (StatusCode::OK, Json(offer_view)).into_response(), + Ok(None) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } +} diff --git a/agent_api_rest/src/holder/holder/offers/reject.rs b/agent_api_rest/src/holder/holder/offers/reject.rs new file mode 100644 index 00000000..eb0ffe17 --- /dev/null +++ b/agent_api_rest/src/holder/holder/offers/reject.rs @@ -0,0 +1,24 @@ +use agent_holder::{offer::command::OfferCommand, state::HolderState}; +use agent_shared::handlers::command_handler; +use axum::{ + extract::{Path, State}, + response::{IntoResponse, Response}, +}; +use hyper::StatusCode; + +#[axum_macros::debug_handler] +pub(crate) async fn reject(State(state): State, Path(offer_id): Path) -> Response { + let command = OfferCommand::RejectCredentialOffer { + offer_id: offer_id.clone(), + }; + + // Remove the Credential Offer from the state. + if command_handler(&offer_id, &state.command.offer, command).await.is_err() { + // TODO: add better Error responses. This needs to be done properly in all endpoints once + // https://github.com/impierce/openid4vc/issues/78 is fixed. + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + + // TODO: What do we return here? + StatusCode::OK.into_response() +} diff --git a/agent_api_rest/src/holder/mod.rs b/agent_api_rest/src/holder/mod.rs new file mode 100644 index 00000000..bc85cc95 --- /dev/null +++ b/agent_api_rest/src/holder/mod.rs @@ -0,0 +1,5 @@ +pub mod holder; +pub mod openid4vci; + +// pub const TEMP_OFFER_ID: &str = "FIX THIS HARDCODED STRING"; +pub const TEMP_OFFER_ID: &str = "001"; diff --git a/agent_api_rest/src/holder/openid4vci/mod.rs b/agent_api_rest/src/holder/openid4vci/mod.rs new file mode 100644 index 00000000..5bcf023c --- /dev/null +++ b/agent_api_rest/src/holder/openid4vci/mod.rs @@ -0,0 +1,44 @@ +use agent_holder::{offer::command::OfferCommand, state::HolderState}; +use agent_shared::handlers::command_handler; +use axum::{ + extract::State, + response::{IntoResponse, Response}, + Json, +}; +use hyper::StatusCode; +use oid4vci::credential_offer::CredentialOffer; +use serde::{Deserialize, Serialize}; +use tracing::info; + +use crate::holder::TEMP_OFFER_ID; + +#[derive(Deserialize, Serialize)] +pub struct Oid4vciOfferEndpointRequest { + #[serde(flatten)] + pub credential_offer: CredentialOffer, +} + +#[axum_macros::debug_handler] +pub(crate) async fn offers(State(state): State, Json(payload): Json) -> Response { + info!("Request Body: {}", payload); + + let Ok(Oid4vciOfferEndpointRequest { credential_offer }) = serde_json::from_value(payload) else { + return (StatusCode::BAD_REQUEST, "invalid payload").into_response(); + }; + + let offer_id = TEMP_OFFER_ID.to_string(); + // let offer_id = uuid::Uuid::new_v4().to_string(); + + let command = OfferCommand::ReceiveCredentialOffer { + offer_id: offer_id.clone(), + credential_offer, + }; + + // Add the Credential Offer to the state. + match command_handler(&offer_id, &state.command.offer, command).await { + Ok(_) => StatusCode::OK.into_response(), + // TODO: add better Error responses. This needs to be done properly in all endpoints once + // https://github.com/impierce/openid4vc/issues/78 is fixed. + Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } +} diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index 7fcaccbc..953b1c39 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -58,6 +58,16 @@ pub fn app(state: ApplicationState) -> Router { .route("/credentials/:credential_id", get(get_credentials)) .route("/offers", post(offers)) .route("/offers/send", post(send)) + // Agent Holder + .route("/holder/offers", get(holder::holder::offers::offers)) + .route( + "/holder/offers/:offer_id/accept", + post(holder::holder::offers::accept::accept), + ) + .route( + "/holder/offers/:offer_id/reject", + post(holder::holder::offers::reject::reject), + ) // Agent Verification .route("/authorization_requests", post(authorization_requests)) .route( @@ -76,6 +86,7 @@ pub fn app(state: ApplicationState) -> Router { ) .route(&path("/auth/token"), post(token)) .route(&path("/openid4vci/credential"), post(credential)) + .route(&path("/openid4vci/offers"), get(holder::openid4vci::offers)) // SIOPv2 .route(&path("/request/:request_id"), get(request)) .route(&path("/redirect"), post(redirect)) From 2673bacc05af10c66e5d57cfba42bd738178f299 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Tue, 27 Aug 2024 13:21:48 +0200 Subject: [PATCH 10/31] feat: add `AllOffersView` --- .../src/holder/holder/offers/mod.rs | 4 +- agent_api_rest/src/holder/mod.rs | 3 - agent_api_rest/src/holder/openid4vci/mod.rs | 5 +- agent_holder/src/offer/aggregate.rs | 5 +- agent_holder/src/offer/queries/all_offers.rs | 94 +++++++++++++++++++ .../src/offer/{queries.rs => queries/mod.rs} | 5 +- agent_holder/src/state.rs | 16 +++- agent_store/src/in_memory.rs | 15 ++- agent_store/src/postgres.rs | 17 +++- 9 files changed, 139 insertions(+), 25 deletions(-) create mode 100644 agent_holder/src/offer/queries/all_offers.rs rename agent_holder/src/offer/{queries.rs => queries/mod.rs} (95%) diff --git a/agent_api_rest/src/holder/holder/offers/mod.rs b/agent_api_rest/src/holder/holder/offers/mod.rs index 6237a28d..a4fb976f 100644 --- a/agent_api_rest/src/holder/holder/offers/mod.rs +++ b/agent_api_rest/src/holder/holder/offers/mod.rs @@ -10,12 +10,10 @@ use axum::{ }; use hyper::StatusCode; -use crate::holder::TEMP_OFFER_ID; - #[axum_macros::debug_handler] pub(crate) async fn offers(State(state): State) -> Response { // TODO: Add extension that allows for selecting all offers. - match query_handler(TEMP_OFFER_ID, &state.query.offer).await { + match query_handler("all_offers", &state.query.all_offers).await { Ok(Some(offer_view)) => (StatusCode::OK, Json(offer_view)).into_response(), Ok(None) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), diff --git a/agent_api_rest/src/holder/mod.rs b/agent_api_rest/src/holder/mod.rs index bc85cc95..e6a5eca7 100644 --- a/agent_api_rest/src/holder/mod.rs +++ b/agent_api_rest/src/holder/mod.rs @@ -1,5 +1,2 @@ pub mod holder; pub mod openid4vci; - -// pub const TEMP_OFFER_ID: &str = "FIX THIS HARDCODED STRING"; -pub const TEMP_OFFER_ID: &str = "001"; diff --git a/agent_api_rest/src/holder/openid4vci/mod.rs b/agent_api_rest/src/holder/openid4vci/mod.rs index 5bcf023c..95145b61 100644 --- a/agent_api_rest/src/holder/openid4vci/mod.rs +++ b/agent_api_rest/src/holder/openid4vci/mod.rs @@ -10,8 +10,6 @@ use oid4vci::credential_offer::CredentialOffer; use serde::{Deserialize, Serialize}; use tracing::info; -use crate::holder::TEMP_OFFER_ID; - #[derive(Deserialize, Serialize)] pub struct Oid4vciOfferEndpointRequest { #[serde(flatten)] @@ -26,8 +24,7 @@ pub(crate) async fn offers(State(state): State, Json(payload): Json return (StatusCode::BAD_REQUEST, "invalid payload").into_response(); }; - let offer_id = TEMP_OFFER_ID.to_string(); - // let offer_id = uuid::Uuid::new_v4().to_string(); + let offer_id = uuid::Uuid::new_v4().to_string(); let command = OfferCommand::ReceiveCredentialOffer { offer_id: offer_id.clone(), diff --git a/agent_holder/src/offer/aggregate.rs b/agent_holder/src/offer/aggregate.rs index 9efe353a..ae0c74c5 100644 --- a/agent_holder/src/offer/aggregate.rs +++ b/agent_holder/src/offer/aggregate.rs @@ -227,7 +227,10 @@ impl Aggregate for Offer { TokenResponseReceived { token_response, .. } => { self.token_response.replace(token_response); } - CredentialResponseReceived { credentials, .. } => { + CredentialResponseReceived { + status, credentials, .. + } => { + self.status = status; self.credentials = credentials; } CredentialOfferAccepted { status, .. } => { diff --git a/agent_holder/src/offer/queries/all_offers.rs b/agent_holder/src/offer/queries/all_offers.rs new file mode 100644 index 00000000..f19df40f --- /dev/null +++ b/agent_holder/src/offer/queries/all_offers.rs @@ -0,0 +1,94 @@ +use crate::offer::queries::{CustomQuery, Offer, ViewRepository}; +use async_trait::async_trait; +use cqrs_es::{ + persist::{PersistenceError, ViewContext}, + EventEnvelope, Query, View, +}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use std::{collections::HashMap, marker::PhantomData}; + +use super::OfferView; + +const VIEW_ID: &str = "all_offers"; + +/// A custom query trait for the Offer aggregate. This query is used to update the `AllOffersView`. +pub struct AllOffersQuery +where + R: ViewRepository, + V: View, +{ + view_repository: Arc, + _phantom: PhantomData, +} + +impl AllOffersQuery +where + R: ViewRepository, + V: View, +{ + pub fn new(view_repository: Arc) -> Self { + AllOffersQuery { + view_repository, + _phantom: PhantomData, + } + } +} + +#[async_trait] +impl Query for AllOffersQuery +where + R: ViewRepository, + V: View, +{ + // The `dispatch` method is called by the `CqrsFramework` when an event is published. By default `cqrs` will use the + // `aggregate_id` as the `view_id` when calling the `dispatch` method. We override this behavior by using the + // `VIEW_ID` constant as the `view_id`. + async fn dispatch(&self, _view_id: &str, events: &[EventEnvelope]) { + self.apply_events(VIEW_ID, events).await.ok(); + } +} + +#[async_trait] +impl CustomQuery for AllOffersQuery +where + R: ViewRepository, + V: View, +{ + async fn load_mut(&self, view_id: String) -> Result<(V, ViewContext), PersistenceError> { + match self.view_repository.load_with_context(&view_id).await? { + None => { + let view_context = ViewContext::new(view_id, 0); + Ok((Default::default(), view_context)) + } + Some((view, context)) => Ok((view, context)), + } + } + + async fn apply_events(&self, view_id: &str, events: &[EventEnvelope]) -> Result<(), PersistenceError> { + for event in events { + let (mut view, view_context) = self.load_mut(view_id.to_string()).await?; + + view.update(event); + self.view_repository.update_view(view, view_context).await?; + } + Ok(()) + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct AllOffersView { + pub offers: HashMap, +} + +impl View for AllOffersView { + fn update(&mut self, event: &EventEnvelope) { + self.offers + // Get the entry for the aggregate_id + .entry(event.aggregate_id.clone()) + // or insert a new one if it doesn't exist + .or_insert_with(Default::default) + // update the view with the event + .update(event); + } +} diff --git a/agent_holder/src/offer/queries.rs b/agent_holder/src/offer/queries/mod.rs similarity index 95% rename from agent_holder/src/offer/queries.rs rename to agent_holder/src/offer/queries/mod.rs index 43255ffa..0725f0f1 100644 --- a/agent_holder/src/offer/queries.rs +++ b/agent_holder/src/offer/queries/mod.rs @@ -1,5 +1,4 @@ -// pub mod access_token; -// pub mod pre_authorized_code; +pub mod all_offers; use std::collections::HashMap; @@ -17,7 +16,7 @@ use serde::{Deserialize, Serialize}; use crate::offer::aggregate::Offer; -use super::{aggregate::Status, event::OfferEvent}; +use super::aggregate::Status; /// A custom query trait for the Offer aggregate. This trait is used to define custom queries for the Offer aggregate /// that do not make use of `GenericQuery`. diff --git a/agent_holder/src/state.rs b/agent_holder/src/state.rs index de06d0bd..f879a5ed 100644 --- a/agent_holder/src/state.rs +++ b/agent_holder/src/state.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use crate::credential::aggregate::Credential; use crate::credential::queries::CredentialView; use crate::offer::aggregate::Offer; +use crate::offer::queries::all_offers::AllOffersView; use crate::offer::queries::OfferView; use axum::extract::FromRef; @@ -30,15 +31,21 @@ pub struct CommandHandlers { /// This type is used to define the queries that are used to query the view repositories. We make use of `dyn` here, so /// that any type of repository that implements the `ViewRepository` trait can be used, but the corresponding `View` and /// `Aggregate` types must be the same. -type Queries = ViewRepositories, dyn ViewRepository>; +type Queries = ViewRepositories< + dyn ViewRepository, + dyn ViewRepository, + dyn ViewRepository, +>; -pub struct ViewRepositories +pub struct ViewRepositories where C: ViewRepository + ?Sized, - O: ViewRepository + ?Sized, + O1: ViewRepository + ?Sized, + O2: ViewRepository + ?Sized, { pub credential: Arc, - pub offer: Arc, + pub offer: Arc, + pub all_offers: Arc, } impl Clone for Queries { @@ -46,6 +53,7 @@ impl Clone for Queries { ViewRepositories { credential: self.credential.clone(), offer: self.offer.clone(), + all_offers: self.all_offers.clone(), } } } diff --git a/agent_store/src/in_memory.rs b/agent_store/src/in_memory.rs index 2eb90f46..316019e0 100644 --- a/agent_store/src/in_memory.rs +++ b/agent_store/src/in_memory.rs @@ -1,4 +1,4 @@ -use agent_holder::{services::HolderServices, state::HolderState}; +use agent_holder::{offer::queries::all_offers::AllOffersQuery, services::HolderServices, state::HolderState}; use agent_issuance::{ offer::{ aggregate::Offer, @@ -181,6 +181,10 @@ pub async fn holder_state( // Initialize the in-memory repositories. let credential = Arc::new(MemRepository::default()); let offer = Arc::new(MemRepository::default()); + let all_offers = Arc::new(MemRepository::default()); + + // Create custom-queries for the offer aggregate. + let all_offers_query = AllOffersQuery::new(all_offers.clone()); // Partition the event_publishers into the different aggregates. let (_, _, _, credential_event_publishers, offer_event_publishers, _, _) = @@ -200,12 +204,17 @@ pub async fn holder_state( offer_event_publishers.into_iter().fold( AggregateHandler::new(holder_services.clone()) .append_query(SimpleLoggingQuery {}) - .append_query(generic_query(offer.clone())), + .append_query(generic_query(offer.clone())) + .append_query(all_offers_query), |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), ), ), }, - query: agent_holder::state::ViewRepositories { credential, offer }, + query: agent_holder::state::ViewRepositories { + credential, + offer, + all_offers, + }, } } diff --git a/agent_store/src/postgres.rs b/agent_store/src/postgres.rs index bd121db6..ef2cc880 100644 --- a/agent_store/src/postgres.rs +++ b/agent_store/src/postgres.rs @@ -1,4 +1,4 @@ -use agent_holder::{services::HolderServices, state::HolderState}; +use agent_holder::{offer::queries::all_offers::AllOffersQuery, services::HolderServices, state::HolderState}; use agent_issuance::{ offer::queries::{access_token::AccessTokenQuery, pre_authorized_code::PreAuthorizedCodeQuery}, services::IssuanceServices, @@ -136,10 +136,14 @@ pub async fn holder_state( ); let pool = default_postgress_pool(&connection_string).await; - // Initialize the in-memory repositories. + // Initialize the postgres repositories. let credential: Arc> = Arc::new(PostgresViewRepository::new("holder_credential", pool.clone())); let offer = Arc::new(PostgresViewRepository::new("received_offer", pool.clone())); + let all_offers = Arc::new(PostgresViewRepository::new("all_offers", pool.clone())); + + // Create custom-queries for the offer aggregate. + let all_offers_query = AllOffersQuery::new(all_offers.clone()); // Partition the event_publishers into the different aggregates. let (_, _, _, credential_event_publishers, offer_event_publishers, _, _) = @@ -159,12 +163,17 @@ pub async fn holder_state( offer_event_publishers.into_iter().fold( AggregateHandler::new(pool, holder_services.clone()) .append_query(SimpleLoggingQuery {}) - .append_query(generic_query(offer.clone())), + .append_query(generic_query(offer.clone())) + .append_query(all_offers_query), |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), ), ), }, - query: agent_holder::state::ViewRepositories { credential, offer }, + query: agent_holder::state::ViewRepositories { + credential, + offer, + all_offers, + }, } } From 5ee0ae4319a0bbc5cd79ba2470feefc279df62bb Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Tue, 27 Aug 2024 15:10:28 +0200 Subject: [PATCH 11/31] feat: add Holder views to `init.sql` --- agent_application/docker/db/init.sql | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/agent_application/docker/db/init.sql b/agent_application/docker/db/init.sql index 0989bb4e..d1ccbc36 100644 --- a/agent_application/docker/db/init.sql +++ b/agent_application/docker/db/init.sql @@ -50,6 +50,30 @@ CREATE TABLE server_config PRIMARY KEY (view_id) ); +CREATE TABLE received_offer +( + view_id text NOT NULL, + version bigint CHECK (version >= 0) NOT NULL, + payload json NOT NULL, + PRIMARY KEY (view_id) +); + +CREATE TABLE all_offers +( + view_id text NOT NULL, + version bigint CHECK (version >= 0) NOT NULL, + payload json NOT NULL, + PRIMARY KEY (view_id) +); + +CREATE TABLE holder_credential +( + view_id text NOT NULL, + version bigint CHECK (version >= 0) NOT NULL, + payload json NOT NULL, + PRIMARY KEY (view_id) +); + CREATE TABLE authorization_request ( view_id text NOT NULL, From 1babdd47247370446d4f3d5d9468c37245ec3d01 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Tue, 27 Aug 2024 15:40:58 +0200 Subject: [PATCH 12/31] fix: fix `OfferView` update --- agent_holder/src/offer/aggregate.rs | 6 +++--- agent_holder/src/offer/queries/mod.rs | 15 +++++++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/agent_holder/src/offer/aggregate.rs b/agent_holder/src/offer/aggregate.rs index ae0c74c5..aae79a9d 100644 --- a/agent_holder/src/offer/aggregate.rs +++ b/agent_holder/src/offer/aggregate.rs @@ -224,6 +224,9 @@ impl Aggregate for Offer { self.credential_offer.replace(credential_offer); self.credential_configurations.replace(credential_configurations); } + CredentialOfferAccepted { status, .. } => { + self.status = status; + } TokenResponseReceived { token_response, .. } => { self.token_response.replace(token_response); } @@ -233,9 +236,6 @@ impl Aggregate for Offer { self.status = status; self.credentials = credentials; } - CredentialOfferAccepted { status, .. } => { - self.status = status; - } CredentialOfferRejected { status, .. } => { self.status = status; } diff --git a/agent_holder/src/offer/queries/mod.rs b/agent_holder/src/offer/queries/mod.rs index 0725f0f1..98f5bc17 100644 --- a/agent_holder/src/offer/queries/mod.rs +++ b/agent_holder/src/offer/queries/mod.rs @@ -54,14 +54,21 @@ impl View for OfferView { self.credential_configurations .replace(credential_configurations.clone()); } - CredentialOfferAccepted { .. } => {} + CredentialOfferAccepted { status, .. } => { + self.status.clone_from(status); + } TokenResponseReceived { token_response, .. } => { self.token_response.replace(token_response.clone()); } - CredentialResponseReceived { credentials, .. } => { - self.credentials.extend(credentials.clone()); + CredentialResponseReceived { + status, credentials, .. + } => { + self.status.clone_from(status); + self.credentials.clone_from(credentials); + } + CredentialOfferRejected { status, .. } => { + self.status.clone_from(status); } - CredentialOfferRejected { .. } => todo!(), } } } From 7bcc7308a2ed287b99e98055089de5d10ef6160c Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Tue, 27 Aug 2024 21:22:55 +0200 Subject: [PATCH 13/31] feat: add credentials endpoint for Holder --- .../src/holder/holder/credentials/mod.rs | 18 ++++ agent_api_rest/src/holder/holder/mod.rs | 1 + agent_api_rest/src/lib.rs | 1 + agent_application/docker/db/init.sql | 9 ++ .../src/credential/queries/all_credentials.rs | 94 +++++++++++++++++++ .../credential/{queries.rs => queries/mod.rs} | 24 ++++- agent_holder/src/state.rs | 11 ++- agent_store/src/in_memory.rs | 11 ++- agent_store/src/postgres.rs | 12 ++- 9 files changed, 171 insertions(+), 10 deletions(-) create mode 100644 agent_api_rest/src/holder/holder/credentials/mod.rs create mode 100644 agent_holder/src/credential/queries/all_credentials.rs rename agent_holder/src/credential/{queries.rs => queries/mod.rs} (53%) diff --git a/agent_api_rest/src/holder/holder/credentials/mod.rs b/agent_api_rest/src/holder/holder/credentials/mod.rs new file mode 100644 index 00000000..806e96a1 --- /dev/null +++ b/agent_api_rest/src/holder/holder/credentials/mod.rs @@ -0,0 +1,18 @@ +use agent_holder::state::HolderState; +use agent_shared::handlers::query_handler; +use axum::{ + extract::State, + response::{IntoResponse, Response}, + Json, +}; +use hyper::StatusCode; + +#[axum_macros::debug_handler] +pub(crate) async fn credentials(State(state): State) -> Response { + // TODO: Add extension that allows for selecting all credentials. + match query_handler("all_credentials", &state.query.all_credentials).await { + Ok(Some(offer_view)) => (StatusCode::OK, Json(offer_view)).into_response(), + Ok(None) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } +} diff --git a/agent_api_rest/src/holder/holder/mod.rs b/agent_api_rest/src/holder/holder/mod.rs index 4791300f..1a09baa0 100644 --- a/agent_api_rest/src/holder/holder/mod.rs +++ b/agent_api_rest/src/holder/holder/mod.rs @@ -1 +1,2 @@ +pub mod credentials; pub mod offers; diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index 953b1c39..ab3165b0 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -59,6 +59,7 @@ pub fn app(state: ApplicationState) -> Router { .route("/offers", post(offers)) .route("/offers/send", post(send)) // Agent Holder + .route("/holder/credentials", get(holder::holder::credentials::credentials)) .route("/holder/offers", get(holder::holder::offers::offers)) .route( "/holder/offers/:offer_id/accept", diff --git a/agent_application/docker/db/init.sql b/agent_application/docker/db/init.sql index d1ccbc36..f333905c 100644 --- a/agent_application/docker/db/init.sql +++ b/agent_application/docker/db/init.sql @@ -74,6 +74,15 @@ CREATE TABLE holder_credential PRIMARY KEY (view_id) ); + +CREATE TABLE all_credentials +( + view_id text NOT NULL, + version bigint CHECK (version >= 0) NOT NULL, + payload json NOT NULL, + PRIMARY KEY (view_id) +); + CREATE TABLE authorization_request ( view_id text NOT NULL, diff --git a/agent_holder/src/credential/queries/all_credentials.rs b/agent_holder/src/credential/queries/all_credentials.rs new file mode 100644 index 00000000..4f9b78fd --- /dev/null +++ b/agent_holder/src/credential/queries/all_credentials.rs @@ -0,0 +1,94 @@ +use crate::credential::queries::{Credential, CustomQuery, ViewRepository}; +use async_trait::async_trait; +use cqrs_es::{ + persist::{PersistenceError, ViewContext}, + EventEnvelope, Query, View, +}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use std::{collections::HashMap, marker::PhantomData}; + +use super::CredentialView; + +const VIEW_ID: &str = "all_credentials"; + +/// A custom query trait for the Credential aggregate. This query is used to update the `AllCredentialsView`. +pub struct AllCredentialsQuery +where + R: ViewRepository, + V: View, +{ + view_repository: Arc, + _phantom: PhantomData, +} + +impl AllCredentialsQuery +where + R: ViewRepository, + V: View, +{ + pub fn new(view_repository: Arc) -> Self { + AllCredentialsQuery { + view_repository, + _phantom: PhantomData, + } + } +} + +#[async_trait] +impl Query for AllCredentialsQuery +where + R: ViewRepository, + V: View, +{ + // The `dispatch` method is called by the `CqrsFramework` when an event is published. By default `cqrs` will use the + // `aggregate_id` as the `view_id` when calling the `dispatch` method. We override this behavior by using the + // `VIEW_ID` constant as the `view_id`. + async fn dispatch(&self, _view_id: &str, events: &[EventEnvelope]) { + self.apply_events(VIEW_ID, events).await.ok(); + } +} + +#[async_trait] +impl CustomQuery for AllCredentialsQuery +where + R: ViewRepository, + V: View, +{ + async fn load_mut(&self, view_id: String) -> Result<(V, ViewContext), PersistenceError> { + match self.view_repository.load_with_context(&view_id).await? { + None => { + let view_context = ViewContext::new(view_id, 0); + Ok((Default::default(), view_context)) + } + Some((view, context)) => Ok((view, context)), + } + } + + async fn apply_events(&self, view_id: &str, events: &[EventEnvelope]) -> Result<(), PersistenceError> { + for event in events { + let (mut view, view_context) = self.load_mut(view_id.to_string()).await?; + + view.update(event); + self.view_repository.update_view(view, view_context).await?; + } + Ok(()) + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct AllCredentialsView { + pub credentials: HashMap, +} + +impl View for AllCredentialsView { + fn update(&mut self, event: &EventEnvelope) { + self.credentials + // Get the entry for the aggregate_id + .entry(event.aggregate_id.clone()) + // or insert a new one if it doesn't exist + .or_insert_with(Default::default) + // update the view with the event + .update(event); + } +} diff --git a/agent_holder/src/credential/queries.rs b/agent_holder/src/credential/queries/mod.rs similarity index 53% rename from agent_holder/src/credential/queries.rs rename to agent_holder/src/credential/queries/mod.rs index 164263a9..e60e497a 100644 --- a/agent_holder/src/credential/queries.rs +++ b/agent_holder/src/credential/queries/mod.rs @@ -1,9 +1,27 @@ -use super::{entity::Data, event::CredentialEvent}; +pub mod all_credentials; + +use super::event::CredentialEvent; use crate::credential::aggregate::Credential; -use cqrs_es::{EventEnvelope, View}; -use oid4vci::credential_issuer::credential_configurations_supported::CredentialConfigurationsSupportedObject; +use axum::async_trait; +use cqrs_es::{ + persist::{PersistenceError, ViewContext, ViewRepository}, + EventEnvelope, Query, View, +}; use serde::{Deserialize, Serialize}; +/// A custom query trait for the Credential aggregate. This trait is used to define custom queries for the Credential aggregate +/// that do not make use of `GenericQuery`. +#[async_trait] +pub trait CustomQuery: Query +where + R: ViewRepository, + V: View, +{ + async fn load_mut(&self, view_id: String) -> Result<(V, ViewContext), PersistenceError>; + + async fn apply_events(&self, view_id: &str, events: &[EventEnvelope]) -> Result<(), PersistenceError>; +} + #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct CredentialView { pub credential_id: Option, diff --git a/agent_holder/src/state.rs b/agent_holder/src/state.rs index f879a5ed..f1bf37b6 100644 --- a/agent_holder/src/state.rs +++ b/agent_holder/src/state.rs @@ -3,6 +3,7 @@ use cqrs_es::persist::ViewRepository; use std::sync::Arc; use crate::credential::aggregate::Credential; +use crate::credential::queries::all_credentials::AllCredentialsView; use crate::credential::queries::CredentialView; use crate::offer::aggregate::Offer; use crate::offer::queries::all_offers::AllOffersView; @@ -33,17 +34,20 @@ pub struct CommandHandlers { /// `Aggregate` types must be the same. type Queries = ViewRepositories< dyn ViewRepository, + dyn ViewRepository, dyn ViewRepository, dyn ViewRepository, >; -pub struct ViewRepositories +pub struct ViewRepositories where - C: ViewRepository + ?Sized, + C1: ViewRepository + ?Sized, + C2: ViewRepository + ?Sized, O1: ViewRepository + ?Sized, O2: ViewRepository + ?Sized, { - pub credential: Arc, + pub credential: Arc, + pub all_credentials: Arc, pub offer: Arc, pub all_offers: Arc, } @@ -52,6 +56,7 @@ impl Clone for Queries { fn clone(&self) -> Self { ViewRepositories { credential: self.credential.clone(), + all_credentials: self.all_credentials.clone(), offer: self.offer.clone(), all_offers: self.all_offers.clone(), } diff --git a/agent_store/src/in_memory.rs b/agent_store/src/in_memory.rs index 316019e0..44db81e5 100644 --- a/agent_store/src/in_memory.rs +++ b/agent_store/src/in_memory.rs @@ -1,4 +1,7 @@ -use agent_holder::{offer::queries::all_offers::AllOffersQuery, services::HolderServices, state::HolderState}; +use agent_holder::{ + credential::queries::all_credentials::AllCredentialsQuery, offer::queries::all_offers::AllOffersQuery, + services::HolderServices, state::HolderState, +}; use agent_issuance::{ offer::{ aggregate::Offer, @@ -181,9 +184,11 @@ pub async fn holder_state( // Initialize the in-memory repositories. let credential = Arc::new(MemRepository::default()); let offer = Arc::new(MemRepository::default()); + let all_credentials = Arc::new(MemRepository::default()); let all_offers = Arc::new(MemRepository::default()); // Create custom-queries for the offer aggregate. + let all_credentials_query = AllCredentialsQuery::new(all_credentials.clone()); let all_offers_query = AllOffersQuery::new(all_offers.clone()); // Partition the event_publishers into the different aggregates. @@ -196,7 +201,8 @@ pub async fn holder_state( credential_event_publishers.into_iter().fold( AggregateHandler::new(holder_services.clone()) .append_query(SimpleLoggingQuery {}) - .append_query(generic_query(credential.clone())), + .append_query(generic_query(credential.clone())) + .append_query(all_credentials_query), |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), ), ), @@ -212,6 +218,7 @@ pub async fn holder_state( }, query: agent_holder::state::ViewRepositories { credential, + all_credentials, offer, all_offers, }, diff --git a/agent_store/src/postgres.rs b/agent_store/src/postgres.rs index ef2cc880..4c6adb97 100644 --- a/agent_store/src/postgres.rs +++ b/agent_store/src/postgres.rs @@ -1,4 +1,7 @@ -use agent_holder::{offer::queries::all_offers::AllOffersQuery, services::HolderServices, state::HolderState}; +use agent_holder::{ + credential::queries::all_credentials::AllCredentialsQuery, offer::queries::all_offers::AllOffersQuery, + services::HolderServices, state::HolderState, +}; use agent_issuance::{ offer::queries::{access_token::AccessTokenQuery, pre_authorized_code::PreAuthorizedCodeQuery}, services::IssuanceServices, @@ -139,10 +142,13 @@ pub async fn holder_state( // Initialize the postgres repositories. let credential: Arc> = Arc::new(PostgresViewRepository::new("holder_credential", pool.clone())); + let all_credentials: Arc> = + Arc::new(PostgresViewRepository::new("all_credentials", pool.clone())); let offer = Arc::new(PostgresViewRepository::new("received_offer", pool.clone())); let all_offers = Arc::new(PostgresViewRepository::new("all_offers", pool.clone())); // Create custom-queries for the offer aggregate. + let all_credentials_query = AllCredentialsQuery::new(all_credentials.clone()); let all_offers_query = AllOffersQuery::new(all_offers.clone()); // Partition the event_publishers into the different aggregates. @@ -155,7 +161,8 @@ pub async fn holder_state( credential_event_publishers.into_iter().fold( AggregateHandler::new(pool.clone(), holder_services.clone()) .append_query(SimpleLoggingQuery {}) - .append_query(generic_query(credential.clone())), + .append_query(generic_query(credential.clone())) + .append_query(all_credentials_query), |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), ), ), @@ -171,6 +178,7 @@ pub async fn holder_state( }, query: agent_holder::state::ViewRepositories { credential, + all_credentials, offer, all_offers, }, From 2000769309d5552a523b97a5dd7405335f3ebe5a Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Tue, 27 Aug 2024 23:43:06 +0200 Subject: [PATCH 14/31] refactor: refactor Router --- Cargo.lock | 53 ++++++----- agent_api_rest/src/holder/mod.rs | 24 +++++ .../issuance/credential_issuer/credential.rs | 50 +++++----- .../src/issuance/credential_issuer/token.rs | 9 +- .../well_known/oauth_authorization_server.rs | 8 +- .../well_known/openid_credential_issuer.rs | 8 +- agent_api_rest/src/issuance/credentials.rs | 13 +-- agent_api_rest/src/issuance/mod.rs | 34 +++++++ agent_api_rest/src/issuance/offers/mod.rs | 11 +-- agent_api_rest/src/lib.rs | 94 +++++-------------- .../verification/authorization_requests.rs | 9 +- agent_api_rest/src/verification/mod.rs | 26 +++++ .../verification/relying_party/redirect.rs | 11 +-- .../src/verification/relying_party/request.rs | 9 +- agent_application/src/main.rs | 8 +- agent_holder/Cargo.toml | 26 ++--- agent_holder/src/credential/queries/mod.rs | 2 +- agent_holder/src/state.rs | 7 -- agent_issuance/Cargo.toml | 1 - agent_issuance/src/state.rs | 7 -- agent_verification/Cargo.toml | 1 - agent_verification/src/state.rs | 8 -- 22 files changed, 200 insertions(+), 219 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9ead83a..bc1db0a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,7 +74,7 @@ dependencies = [ "axum-macros", "futures", "http-api-problem", - "hyper 1.3.1", + "hyper 1.4.1", "jsonwebtoken", "lazy_static", "mime", @@ -151,25 +151,36 @@ dependencies = [ name = "agent_holder" version = "0.1.0" dependencies = [ + "agent_api_rest", + "agent_holder", + "agent_issuance", "agent_secret_manager", "agent_shared", + "agent_store", + "agent_verification", + "async-std", "async-trait", - "axum 0.7.5", "chrono", "cqrs-es", "derivative", + "did_manager", "futures", "identity_core", "identity_credential", "jsonschema", "jsonwebtoken", + "lazy_static", "oid4vc-core", "oid4vc-manager", "oid4vci", + "rstest", "serde", "serde_json", + "serial_test", "thiserror", + "tokio", "tracing", + "tracing-test", "types-ob-v3", "url", "uuid", @@ -184,7 +195,6 @@ dependencies = [ "agent_shared", "async-std", "async-trait", - "axum 0.7.5", "chrono", "cqrs-es", "derivative", @@ -296,7 +306,6 @@ dependencies = [ "anyhow", "async-std", "async-trait", - "axum 0.7.5", "cqrs-es", "did_manager", "futures", @@ -754,7 +763,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", "itoa", "matchit", @@ -1115,9 +1124,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "camino" @@ -3154,9 +3163,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", @@ -3195,7 +3204,7 @@ checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", "rustls 0.23.10", "rustls-pki-types", @@ -3216,7 +3225,7 @@ dependencies = [ "futures-util", "http 1.1.0", "http-body 1.0.0", - "hyper 1.3.1", + "hyper 1.4.1", "pin-project-lite", "socket2 0.5.7", "tokio", @@ -4472,13 +4481,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.11" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi 0.3.9", "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -5750,7 +5760,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-rustls 0.27.2", "hyper-util", "ipnet", @@ -7576,28 +7586,27 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot 0.12.3", "pin-project-lite", "signal-hook-registry", "socket2 0.5.7", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", @@ -8546,7 +8555,7 @@ dependencies = [ "futures", "http 1.1.0", "http-body-util", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", "log", "once_cell", diff --git a/agent_api_rest/src/holder/mod.rs b/agent_api_rest/src/holder/mod.rs index e6a5eca7..fcd77932 100644 --- a/agent_api_rest/src/holder/mod.rs +++ b/agent_api_rest/src/holder/mod.rs @@ -1,2 +1,26 @@ pub mod holder; pub mod openid4vci; + +use agent_holder::state::HolderState; +use axum::routing::get; +use axum::{routing::post, Router}; + +use crate::holder::holder::{ + credentials::credentials, + offers::{accept::accept, reject::reject, *}, +}; +use crate::API_VERSION; + +pub fn router(holder_state: HolderState) -> Router { + Router::new() + .nest( + API_VERSION, + Router::new() + .route("/holder/credentials", get(credentials)) + .route("/holder/offers", get(offers)) + .route("/holder/offers/:offer_id/accept", post(accept)) + .route("/holder/offers/:offer_id/reject", post(reject)), + ) + .route("/openid4vci/offers", get(openid4vci::offers)) + .with_state(holder_state) +} diff --git a/agent_api_rest/src/issuance/credential_issuer/credential.rs b/agent_api_rest/src/issuance/credential_issuer/credential.rs index 91571bce..81d1b4aa 100644 --- a/agent_api_rest/src/issuance/credential_issuer/credential.rs +++ b/agent_api_rest/src/issuance/credential_issuer/credential.rs @@ -144,7 +144,6 @@ mod tests { use std::sync::Arc; use crate::{ - app, issuance::{ credential_issuer::token::tests::token, credentials::CredentialsEndpointRequest, offers::tests::offers, }, @@ -153,14 +152,13 @@ mod tests { use super::*; use crate::issuance::credentials::tests::credentials; + use crate::issuance::router; use crate::API_VERSION; use agent_event_publisher_http::EventPublisherHttp; - use agent_holder::services::test_utils::test_holder_services; use agent_issuance::services::test_utils::test_issuance_services; use agent_issuance::{offer::event::OfferEvent, startup_commands::startup_commands, state::initialize}; use agent_shared::config::{set_config, Events}; use agent_store::{in_memory, EventPublisher}; - use agent_verification::services::test_utils::test_verification_services; use axum::{ body::Body, http::{self, Request}, @@ -278,36 +276,30 @@ mod tests { #[case] is_self_signed: bool, #[case] delay: u64, ) { - let (external_server, issuance_event_publishers, holder_event_publishers, verification_event_publishers) = - if with_external_server { - let external_server = MockServer::start().await; - - let target_url = format!("{}/ssi-events-subscriber", &external_server.uri()); - - set_config().enable_event_publisher_http(); - set_config().set_event_publisher_http_target_url(target_url.clone()); - set_config().set_event_publisher_http_target_events(Events { - offer: vec![agent_shared::config::OfferEvent::CredentialRequestVerified], - ..Default::default() - }); - - ( - Some(external_server), - vec![Box::new(EventPublisherHttp::load().unwrap()) as Box], - vec![Box::new(EventPublisherHttp::load().unwrap()) as Box], - vec![Box::new(EventPublisherHttp::load().unwrap()) as Box], - ) - } else { - (None, Default::default(), Default::default(), Default::default()) - }; + let (external_server, issuance_event_publishers) = if with_external_server { + let external_server = MockServer::start().await; + + let target_url = format!("{}/ssi-events-subscriber", &external_server.uri()); + + set_config().enable_event_publisher_http(); + set_config().set_event_publisher_http_target_url(target_url.clone()); + set_config().set_event_publisher_http_target_events(Events { + offer: vec![agent_shared::config::OfferEvent::CredentialRequestVerified], + ..Default::default() + }); + + ( + Some(external_server), + vec![Box::new(EventPublisherHttp::load().unwrap()) as Box], + ) + } else { + (None, Default::default()) + }; let issuance_state = in_memory::issuance_state(test_issuance_services(), issuance_event_publishers).await; - let holder_state = in_memory::holder_state(test_holder_services(), holder_event_publishers).await; - let verification_state = - in_memory::verification_state(test_verification_services(), verification_event_publishers).await; initialize(&issuance_state, startup_commands(BASE_URL.clone())).await; - let mut app = app((issuance_state, holder_state, verification_state)); + let mut app = router(issuance_state); if let Some(external_server) = &external_server { external_server diff --git a/agent_api_rest/src/issuance/credential_issuer/token.rs b/agent_api_rest/src/issuance/credential_issuer/token.rs index a9ec9154..34f9f903 100644 --- a/agent_api_rest/src/issuance/credential_issuer/token.rs +++ b/agent_api_rest/src/issuance/credential_issuer/token.rs @@ -61,18 +61,15 @@ pub(crate) async fn token( #[cfg(test)] pub mod tests { use crate::{ - app, - issuance::{credentials::tests::credentials, offers::tests::offers}, + issuance::{credentials::tests::credentials, offers::tests::offers, router}, tests::BASE_URL, }; use super::*; - use agent_holder::services::test_utils::test_holder_services; use agent_issuance::{ services::test_utils::test_issuance_services, startup_commands::startup_commands, state::initialize, }; use agent_store::in_memory; - use agent_verification::services::test_utils::test_verification_services; use axum::{ body::Body, http::{self, Request}, @@ -114,11 +111,9 @@ pub mod tests { #[tokio::test] async fn test_token_endpoint() { let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; - let holder_state = in_memory::holder_state(test_holder_services(), Default::default()).await; - let verification_state = in_memory::verification_state(test_verification_services(), Default::default()).await; initialize(&issuance_state, startup_commands(BASE_URL.clone())).await; - let mut app = app((issuance_state, holder_state, verification_state)); + let mut app = router(issuance_state); credentials(&mut app).await; let pre_authorized_code = offers(&mut app).await; diff --git a/agent_api_rest/src/issuance/credential_issuer/well_known/oauth_authorization_server.rs b/agent_api_rest/src/issuance/credential_issuer/well_known/oauth_authorization_server.rs index e40c8fb2..723c6e25 100644 --- a/agent_api_rest/src/issuance/credential_issuer/well_known/oauth_authorization_server.rs +++ b/agent_api_rest/src/issuance/credential_issuer/well_known/oauth_authorization_server.rs @@ -23,15 +23,13 @@ pub(crate) async fn oauth_authorization_server(State(state): State) mod tests { use std::collections::HashMap; - use crate::{app, tests::BASE_URL}; + use crate::{issuance::router, tests::BASE_URL}; use super::*; - use agent_holder::services::test_utils::test_holder_services; use agent_issuance::{ services::test_utils::test_issuance_services, startup_commands::startup_commands, state::initialize, }; use agent_shared::UrlAppendHelpers; use agent_store::in_memory; - use agent_verification::services::test_utils::test_verification_services; use axum::{ body::Body, http::{self, Request}, @@ -135,11 +133,9 @@ mod tests { #[tokio::test] async fn test_openid_credential_issuer_endpoint() { let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; - let holder_state = in_memory::holder_state(test_holder_services(), Default::default()).await; - let verification_state = in_memory::verification_state(test_verification_services(), Default::default()).await; initialize(&issuance_state, startup_commands(BASE_URL.clone())).await; - let mut app = app((issuance_state, holder_state, verification_state)); + let mut app = router(issuance_state); let _credential_issuer_metadata = openid_credential_issuer(&mut app).await; } diff --git a/agent_api_rest/src/issuance/credentials.rs b/agent_api_rest/src/issuance/credentials.rs index 6152a940..71eb920e 100644 --- a/agent_api_rest/src/issuance/credentials.rs +++ b/agent_api_rest/src/issuance/credentials.rs @@ -163,16 +163,12 @@ pub(crate) async fn credentials( #[cfg(test)] pub mod tests { use super::*; + use crate::issuance::router; + use crate::tests::{BASE_URL, CREDENTIAL_CONFIGURATION_ID, OFFER_ID}; use crate::API_VERSION; - use crate::{ - app, - tests::{BASE_URL, CREDENTIAL_CONFIGURATION_ID, OFFER_ID}, - }; - use agent_holder::services::test_utils::test_holder_services; use agent_issuance::services::test_utils::test_issuance_services; use agent_issuance::{startup_commands::startup_commands, state::initialize}; use agent_store::in_memory; - use agent_verification::services::test_utils::test_verification_services; use axum::{ body::Body, http::{self, Request}, @@ -265,12 +261,9 @@ pub mod tests { #[tracing_test::traced_test] async fn test_credentials_endpoint() { let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; - let holder_state = in_memory::holder_state(test_holder_services(), Default::default()).await; - let verification_state = in_memory::verification_state(test_verification_services(), Default::default()).await; initialize(&issuance_state, startup_commands(BASE_URL.clone())).await; - let mut app = app((issuance_state, holder_state, verification_state)); - + let mut app = router(issuance_state); credentials(&mut app).await; } } diff --git a/agent_api_rest/src/issuance/mod.rs b/agent_api_rest/src/issuance/mod.rs index 954f1c40..0bf4064e 100644 --- a/agent_api_rest/src/issuance/mod.rs +++ b/agent_api_rest/src/issuance/mod.rs @@ -1,3 +1,37 @@ pub mod credential_issuer; pub mod credentials; pub mod offers; + +use agent_issuance::state::IssuanceState; +use axum::routing::get; +use axum::{routing::post, Router}; + +use crate::issuance::{ + credential_issuer::{ + credential::credential, token::token, well_known::oauth_authorization_server::oauth_authorization_server, + well_known::openid_credential_issuer::openid_credential_issuer, + }, + credentials::{credentials, get_credentials}, + offers::{offers, send::send}, +}; +use crate::API_VERSION; + +pub fn router(issuance_state: IssuanceState) -> Router { + Router::new() + .nest( + API_VERSION, + Router::new() + .route("/credentials", post(credentials)) + .route("/credentials/:credential_id", get(get_credentials)) + .route("/offers", post(offers)) + .route("/offers/send", post(send)), + ) + .route( + "/.well-known/oauth-authorization-server", + get(oauth_authorization_server), + ) + .route("/.well-known/openid-credential-issuer", get(openid_credential_issuer)) + .route("/auth/token", post(token)) + .route("/openid4vci/credential", post(credential)) + .with_state(issuance_state) +} diff --git a/agent_api_rest/src/issuance/offers/mod.rs b/agent_api_rest/src/issuance/offers/mod.rs index d5a7fdfe..b6bd1dfe 100644 --- a/agent_api_rest/src/issuance/offers/mod.rs +++ b/agent_api_rest/src/issuance/offers/mod.rs @@ -86,19 +86,16 @@ pub mod tests { use std::str::FromStr; use crate::{ - app, - issuance::credentials::tests::credentials, + issuance::{credentials::tests::credentials, router}, tests::{BASE_URL, OFFER_ID}, }; use super::*; use crate::API_VERSION; - use agent_holder::services::test_utils::test_holder_services; use agent_issuance::{ services::test_utils::test_issuance_services, startup_commands::startup_commands, state::initialize, }; use agent_store::in_memory; - use agent_verification::services::test_utils::test_verification_services; use axum::{ body::Body, http::{self, Request}, @@ -160,13 +157,9 @@ pub mod tests { #[tracing_test::traced_test] async fn test_offers_endpoint() { let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; - let holder_state = in_memory::holder_state(test_holder_services(), Default::default()).await; - - let verification_state = in_memory::verification_state(test_verification_services(), Default::default()).await; - initialize(&issuance_state, startup_commands(BASE_URL.clone())).await; - let mut app = app((issuance_state, holder_state, verification_state)); + let mut app = router(issuance_state); credentials(&mut app).await; let _pre_authorized_code = offers(&mut app).await; diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index ab3165b0..d21a3a98 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -6,38 +6,26 @@ use agent_holder::state::HolderState; use agent_issuance::state::IssuanceState; use agent_shared::{config::config, ConfigError}; use agent_verification::state::VerificationState; -use axum::{ - body::Bytes, - extract::MatchedPath, - http::Request, - response::Response, - routing::{get, post}, - Router, -}; -use issuance::credentials::{credentials, get_credentials}; -use issuance::offers::offers; -use issuance::{ - credential_issuer::{ - credential::credential, - token::token, - well_known::{ - oauth_authorization_server::oauth_authorization_server, openid_credential_issuer::openid_credential_issuer, - }, - }, - offers::send::send, -}; +use axum::{body::Bytes, extract::MatchedPath, http::Request, response::Response, Router}; use tower_http::trace::TraceLayer; use tracing::{info_span, Span}; -use verification::{ - authorization_requests::{authorization_requests, get_authorization_requests}, - relying_party::{redirect::redirect, request::request}, -}; pub const API_VERSION: &str = "/v0"; -pub type ApplicationState = (IssuanceState, HolderState, VerificationState); +#[derive(Default)] +pub struct ApplicationState { + pub issuance_state: Option, + pub holder_state: Option, + pub verification_state: Option, +} pub fn app(state: ApplicationState) -> Router { + let ApplicationState { + issuance_state, + holder_state, + verification_state, + } = state; + let base_path = get_base_path(); let path = |suffix: &str| -> String { @@ -48,49 +36,14 @@ pub fn app(state: ApplicationState) -> Router { } }; - // TODO: refactor routes into a nice and consistant folder structure. Router::new() .nest( - &path(API_VERSION), + &path(Default::default()), Router::new() - // Agent Issuance - .route("/credentials", post(credentials)) - .route("/credentials/:credential_id", get(get_credentials)) - .route("/offers", post(offers)) - .route("/offers/send", post(send)) - // Agent Holder - .route("/holder/credentials", get(holder::holder::credentials::credentials)) - .route("/holder/offers", get(holder::holder::offers::offers)) - .route( - "/holder/offers/:offer_id/accept", - post(holder::holder::offers::accept::accept), - ) - .route( - "/holder/offers/:offer_id/reject", - post(holder::holder::offers::reject::reject), - ) - // Agent Verification - .route("/authorization_requests", post(authorization_requests)) - .route( - "/authorization_requests/:authorization_request_id", - get(get_authorization_requests), - ), - ) - // OpenID4VCI Pre-Authorized Code Flow - .route( - &path("/.well-known/oauth-authorization-server"), - get(oauth_authorization_server), + .merge(issuance_state.map(issuance::router).unwrap_or_default()) + .merge(holder_state.map(holder::router).unwrap_or_default()) + .merge(verification_state.map(verification::router).unwrap_or_default()), ) - .route( - &path("/.well-known/openid-credential-issuer"), - get(openid_credential_issuer), - ) - .route(&path("/auth/token"), post(token)) - .route(&path("/openid4vci/credential"), post(credential)) - .route(&path("/openid4vci/offers"), get(holder::openid4vci::offers)) - // SIOPv2 - .route(&path("/request/:request_id"), get(request)) - .route(&path("/redirect"), post(redirect)) // Trace layer .layer( TraceLayer::new_for_http() @@ -114,7 +67,6 @@ pub fn app(state: ApplicationState) -> Router { tracing::info!("Response Body: {}", std::str::from_utf8(chunk).unwrap()); }), ) - .with_state(state) } fn get_base_path() -> Result { @@ -145,10 +97,9 @@ fn get_base_path() -> Result { mod tests { use std::collections::HashMap; - use agent_holder::services::test_utils::test_holder_services; + use super::*; use agent_issuance::services::test_utils::test_issuance_services; use agent_store::in_memory; - use agent_verification::services::test_utils::test_verification_services; use axum::routing::post; use oid4vci::credential_issuer::{ credential_configurations_supported::CredentialConfigurationsSupportedObject, @@ -156,8 +107,6 @@ mod tests { }; use serde_json::json; - use crate::app; - pub const CREDENTIAL_CONFIGURATION_ID: &str = "badge"; pub const OFFER_ID: &str = "00000000-0000-0000-0000-000000000000"; @@ -204,10 +153,11 @@ mod tests { #[should_panic] async fn test_base_path_routes() { let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; - let holder_state = in_memory::holder_state(test_holder_services(), Default::default()).await; - let verification_state = in_memory::verification_state(test_verification_services(), Default::default()).await; std::env::set_var("UNICORE__BASE_PATH", "unicore"); - let router = app((issuance_state, holder_state, verification_state)); + let router = app(ApplicationState { + issuance_state: Some(issuance_state), + ..Default::default() + }); let _ = router.route("/auth/token", post(handler)); } diff --git a/agent_api_rest/src/verification/authorization_requests.rs b/agent_api_rest/src/verification/authorization_requests.rs index 7ef10126..2c75c736 100644 --- a/agent_api_rest/src/verification/authorization_requests.rs +++ b/agent_api_rest/src/verification/authorization_requests.rs @@ -138,9 +138,7 @@ pub(crate) async fn authorization_requests( #[cfg(test)] pub mod tests { use super::*; - use crate::app; - use agent_holder::services::test_utils::test_holder_services; - use agent_issuance::services::test_utils::test_issuance_services; + use crate::verification::router; use agent_store::in_memory; use agent_verification::services::test_utils::test_verification_services; use axum::{ @@ -222,10 +220,9 @@ pub mod tests { #[tokio::test] #[tracing_test::traced_test] async fn test_authorization_requests_endpoint(#[case] by_value: bool) { - let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; - let holder_state = in_memory::holder_state(test_holder_services(), Default::default()).await; let verification_state = in_memory::verification_state(test_verification_services(), Default::default()).await; - let mut app = app((issuance_state, holder_state, verification_state)); + + let mut app = router(verification_state); authorization_requests(&mut app, by_value).await; } diff --git a/agent_api_rest/src/verification/mod.rs b/agent_api_rest/src/verification/mod.rs index 7aa0c137..c7071971 100644 --- a/agent_api_rest/src/verification/mod.rs +++ b/agent_api_rest/src/verification/mod.rs @@ -1,2 +1,28 @@ pub mod authorization_requests; pub mod relying_party; + +use agent_verification::state::VerificationState; +use axum::routing::get; +use axum::{routing::post, Router}; + +use crate::verification::{ + authorization_requests::authorization_requests, authorization_requests::get_authorization_requests, + relying_party::redirect::redirect, relying_party::request::request, +}; +use crate::API_VERSION; + +pub fn router(verification_state: VerificationState) -> Router { + Router::new() + .nest( + API_VERSION, + Router::new() + .route("/authorization_requests", post(authorization_requests)) + .route( + "/authorization_requests/:authorization_request_id", + get(get_authorization_requests), + ), + ) + .route("/request/:request_id", get(request)) + .route("/redirect", post(redirect)) + .with_state(verification_state) +} diff --git a/agent_api_rest/src/verification/relying_party/redirect.rs b/agent_api_rest/src/verification/relying_party/redirect.rs index fb8ddf4e..b3850b3e 100644 --- a/agent_api_rest/src/verification/relying_party/redirect.rs +++ b/agent_api_rest/src/verification/relying_party/redirect.rs @@ -57,13 +57,10 @@ pub mod tests { use std::{str::FromStr, sync::Arc}; use super::*; - use crate::{ - app, - verification::{authorization_requests::tests::authorization_requests, relying_party::request::tests::request}, + use crate::verification::{ + authorization_requests::tests::authorization_requests, relying_party::request::tests::request, router, }; use agent_event_publisher_http::EventPublisherHttp; - use agent_holder::services::test_utils::test_holder_services; - use agent_issuance::services::test_utils::test_issuance_services; use agent_secret_manager::{secret_manager, subject::Subject}; use agent_shared::config::{set_config, Events}; use agent_store::{in_memory, EventPublisher}; @@ -163,11 +160,9 @@ pub mod tests { let event_publishers = vec![Box::new(EventPublisherHttp::load().unwrap()) as Box]; - let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; - let holder_state = in_memory::holder_state(test_holder_services(), Default::default()).await; let verification_state = in_memory::verification_state(test_verification_services(), event_publishers).await; - let mut app = app((issuance_state, holder_state, verification_state)); + let mut app = router(verification_state); let form_url_encoded_authorization_request = authorization_requests(&mut app, false).await; diff --git a/agent_api_rest/src/verification/relying_party/request.rs b/agent_api_rest/src/verification/relying_party/request.rs index 7d98010c..5bf2d2f1 100644 --- a/agent_api_rest/src/verification/relying_party/request.rs +++ b/agent_api_rest/src/verification/relying_party/request.rs @@ -33,9 +33,7 @@ pub(crate) async fn request( #[cfg(test)] pub mod tests { use super::*; - use crate::{app, verification::authorization_requests::tests::authorization_requests}; - use agent_holder::services::test_utils::test_holder_services; - use agent_issuance::services::test_utils::test_issuance_services; + use crate::verification::{authorization_requests::tests::authorization_requests, router}; use agent_store::in_memory; use agent_verification::services::test_utils::test_verification_services; use axum::{ @@ -71,10 +69,9 @@ pub mod tests { #[tokio::test] #[tracing_test::traced_test] async fn test_request_endpoint() { - let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; - let holder_state = in_memory::holder_state(test_holder_services(), Default::default()).await; let verification_state = in_memory::verification_state(test_verification_services(), Default::default()).await; - let mut app = app((issuance_state, holder_state, verification_state)); + + let mut app = router(verification_state); let form_url_encoded_authorization_request = authorization_requests(&mut app, false).await; diff --git a/agent_application/src/main.rs b/agent_application/src/main.rs index 697d276a..9489d46b 100644 --- a/agent_application/src/main.rs +++ b/agent_application/src/main.rs @@ -1,6 +1,6 @@ #![allow(clippy::await_holding_lock)] -use agent_api_rest::app; +use agent_api_rest::{app, ApplicationState}; use agent_event_publisher_http::EventPublisherHttp; use agent_holder::services::HolderServices; use agent_issuance::{services::IssuanceServices, startup_commands::startup_commands, state::initialize}; @@ -70,7 +70,11 @@ async fn main() -> io::Result<()> { initialize(&issuance_state, startup_commands(url.clone())).await; - let mut app = app((issuance_state, holder_state, verification_state)); + let mut app = app(ApplicationState { + issuance_state: Some(issuance_state), + holder_state: Some(holder_state), + verification_state: Some(verification_state), + }); // CORS if config().cors_enabled.unwrap_or(false) { diff --git a/agent_holder/Cargo.toml b/agent_holder/Cargo.toml index d983de73..8395e0e2 100644 --- a/agent_holder/Cargo.toml +++ b/agent_holder/Cargo.toml @@ -9,7 +9,6 @@ agent_shared = { path = "../agent_shared" } agent_secret_manager = { path = "../agent_secret_manager" } async-trait.workspace = true -axum.workspace = true cqrs-es.workspace = true chrono = "0.4" types-ob-v3 = { git = "https://github.com/impierce/digital-credential-data-models.git", rev = "9f16c27" } @@ -29,17 +28,22 @@ tracing.workspace = true url.workspace = true uuid.workspace = true -# [dev-dependencies] -# agent_issuance = { path = ".", features = ["test_utils"] } -# agent_shared = { path = "../agent_shared", features = ["test_utils"] } +[dev-dependencies] +agent_api_rest = { path = "../agent_api_rest" } +agent_holder = { path = ".", features = ["test_utils"] } +agent_issuance = { path = "../agent_issuance", features = ["test_utils"] } +agent_shared = { path = "../agent_shared", features = ["test_utils"] } +agent_store = { path = "../agent_store" } +agent_verification = { path = "../agent_verification", features = ["test_utils"] } -# did_manager.workspace = true -# lazy_static.workspace = true -# serial_test = "3.0" -# tokio.workspace = true -# tracing-test.workspace = true -# async-std = { version = "1.5", features = ["attributes", "tokio1"] } -# rstest.workspace = true +# axum-test = "15.6" +did_manager.workspace = true +lazy_static.workspace = true +serial_test = "3.0" +tokio.workspace = true +tracing-test.workspace = true +async-std = { version = "1.5", features = ["attributes", "tokio1"] } +rstest.workspace = true [features] test_utils = [] diff --git a/agent_holder/src/credential/queries/mod.rs b/agent_holder/src/credential/queries/mod.rs index e60e497a..f9caebf9 100644 --- a/agent_holder/src/credential/queries/mod.rs +++ b/agent_holder/src/credential/queries/mod.rs @@ -2,7 +2,7 @@ pub mod all_credentials; use super::event::CredentialEvent; use crate::credential::aggregate::Credential; -use axum::async_trait; +use async_trait::async_trait; use cqrs_es::{ persist::{PersistenceError, ViewContext, ViewRepository}, EventEnvelope, Query, View, diff --git a/agent_holder/src/state.rs b/agent_holder/src/state.rs index f1bf37b6..2ebbddf4 100644 --- a/agent_holder/src/state.rs +++ b/agent_holder/src/state.rs @@ -8,7 +8,6 @@ use crate::credential::queries::CredentialView; use crate::offer::aggregate::Offer; use crate::offer::queries::all_offers::AllOffersView; use crate::offer::queries::OfferView; -use axum::extract::FromRef; #[derive(Clone)] pub struct HolderState { @@ -16,12 +15,6 @@ pub struct HolderState { pub query: Queries, } -impl FromRef<(I, HolderState, V)> for HolderState { - fn from_ref(application_state: &(I, HolderState, V)) -> HolderState { - application_state.1.clone() - } -} - /// The command handlers are used to execute commands on the aggregates. #[derive(Clone)] pub struct CommandHandlers { diff --git a/agent_issuance/Cargo.toml b/agent_issuance/Cargo.toml index 247df085..76f603e4 100644 --- a/agent_issuance/Cargo.toml +++ b/agent_issuance/Cargo.toml @@ -9,7 +9,6 @@ agent_shared = { path = "../agent_shared" } agent_secret_manager = { path = "../agent_secret_manager" } async-trait.workspace = true -axum.workspace = true cqrs-es.workspace = true chrono = "0.4" types-ob-v3 = { git = "https://github.com/impierce/digital-credential-data-models.git", rev = "9f16c27" } diff --git a/agent_issuance/src/state.rs b/agent_issuance/src/state.rs index a964a764..4384deb6 100644 --- a/agent_issuance/src/state.rs +++ b/agent_issuance/src/state.rs @@ -13,7 +13,6 @@ use crate::offer::queries::OfferView; use crate::server_config::aggregate::ServerConfig; use crate::server_config::command::ServerConfigCommand; use crate::server_config::queries::ServerConfigView; -use axum::extract::FromRef; #[derive(Clone)] pub struct IssuanceState { @@ -21,12 +20,6 @@ pub struct IssuanceState { pub query: Queries, } -impl FromRef<(IssuanceState, H, V)> for IssuanceState { - fn from_ref(application_state: &(IssuanceState, H, V)) -> IssuanceState { - application_state.0.clone() - } -} - /// The command handlers are used to execute commands on the aggregates. #[derive(Clone)] pub struct CommandHandlers { diff --git a/agent_verification/Cargo.toml b/agent_verification/Cargo.toml index 863d4e71..57907114 100644 --- a/agent_verification/Cargo.toml +++ b/agent_verification/Cargo.toml @@ -10,7 +10,6 @@ agent_shared = { path = "../agent_shared" } anyhow = "1.0" async-trait.workspace = true -axum.workspace = true cqrs-es.workspace = true futures.workspace = true jsonwebtoken.workspace = true diff --git a/agent_verification/src/state.rs b/agent_verification/src/state.rs index 03f2570a..00acbddf 100644 --- a/agent_verification/src/state.rs +++ b/agent_verification/src/state.rs @@ -7,20 +7,12 @@ use crate::authorization_request::queries::AuthorizationRequestView; use crate::connection::aggregate::Connection; use crate::connection::queries::ConnectionView; -use axum::extract::FromRef; - #[derive(Clone)] pub struct VerificationState { pub command: CommandHandlers, pub query: Queries, } -impl FromRef<(I, H, VerificationState)> for VerificationState { - fn from_ref(application_state: &(I, H, VerificationState)) -> VerificationState { - application_state.2.clone() - } -} - /// The command handlers are used to execute commands on the aggregates. #[derive(Clone)] pub struct CommandHandlers { From 7599beebbdd42213de6d25ffe65c180956337288 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 30 Aug 2024 02:17:19 +0200 Subject: [PATCH 15/31] test: refactor test framework --- Cargo.lock | 28 +- Cargo.toml | 5 +- agent_api_rest/Cargo.toml | 6 +- .../src/holder/holder/offers/accept.rs | 11 - .../issuance/credential_issuer/credential.rs | 22 +- .../src/issuance/credential_issuer/token.rs | 12 +- .../well_known/oauth_authorization_server.rs | 12 +- .../well_known/openid_credential_issuer.rs | 15 +- agent_api_rest/src/issuance/credentials.rs | 8 +- agent_api_rest/src/issuance/offers/mod.rs | 19 +- agent_api_rest/src/lib.rs | 13 +- .../verification/authorization_requests.rs | 6 +- .../verification/relying_party/redirect.rs | 7 +- .../src/verification/relying_party/request.rs | 6 +- agent_application/src/main.rs | 2 +- agent_holder/Cargo.toml | 16 +- agent_holder/src/credential/aggregate.rs | 291 ++------- agent_holder/src/credential/command.rs | 9 - .../src/credential/queries/all_credentials.rs | 2 +- agent_holder/src/offer/aggregate.rs | 577 ++++++++---------- agent_holder/src/offer/command.rs | 3 - agent_holder/src/offer/event.rs | 2 +- agent_holder/src/offer/queries/all_offers.rs | 2 +- agent_holder/src/offer/queries/mod.rs | 5 +- agent_holder/src/services.rs | 21 +- agent_issuance/Cargo.toml | 16 +- agent_issuance/src/credential/aggregate.rs | 61 +- agent_issuance/src/offer/aggregate.rs | 364 ++++++----- agent_issuance/src/offer/command.rs | 4 +- agent_issuance/src/server_config/aggregate.rs | 144 ++--- agent_issuance/src/server_config/command.rs | 2 +- agent_issuance/src/server_config/event.rs | 2 +- agent_issuance/src/server_config/queries.rs | 2 +- agent_issuance/src/services.rs | 21 +- agent_issuance/src/startup_commands.rs | 4 +- agent_secret_manager/Cargo.toml | 4 + agent_secret_manager/src/lib.rs | 1 + agent_secret_manager/src/service.rs | 19 + agent_shared/Cargo.toml | 2 +- .../src/authorization_request/aggregate.rs | 7 +- .../src/connection/aggregate.rs | 4 +- agent_verification/src/services.rs | 23 +- 42 files changed, 780 insertions(+), 1000 deletions(-) create mode 100644 agent_secret_manager/src/service.rs diff --git a/Cargo.lock b/Cargo.lock index bc1db0a5..6b25ceaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,9 +157,9 @@ dependencies = [ "agent_secret_manager", "agent_shared", "agent_store", - "agent_verification", "async-std", "async-trait", + "axum 0.7.5", "chrono", "cqrs-es", "derivative", @@ -170,15 +170,20 @@ dependencies = [ "jsonschema", "jsonwebtoken", "lazy_static", + "mime", + "names", "oid4vc-core", "oid4vc-manager", "oid4vci", + "rand 0.8.5", + "reqwest 0.12.5", "rstest", "serde", "serde_json", "serial_test", "thiserror", "tokio", + "tower", "tracing", "tracing-test", "types-ob-v3", @@ -190,6 +195,7 @@ dependencies = [ name = "agent_issuance" version = "0.1.0" dependencies = [ + "agent_holder", "agent_issuance", "agent_secret_manager", "agent_shared", @@ -208,13 +214,13 @@ dependencies = [ "oid4vc-core", "oid4vc-manager", "oid4vci", + "once_cell", "reqwest 0.12.5", "rstest", "serde", "serde_json", "serial_test", "thiserror", - "tokio", "tracing", "tracing-test", "types-ob-v3", @@ -4540,6 +4546,15 @@ dependencies = [ "data-encoding-macro", ] +[[package]] +name = "names" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bddcd3bf5144b6392de80e04c347cd7fab2508f6df16a85fc496ecd5cec39bc" +dependencies = [ + "rand 0.8.5", +] + [[package]] name = "nix" version = "0.24.3" @@ -5956,9 +5971,9 @@ dependencies = [ [[package]] name = "rstest" -version = "0.19.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5316d2a1479eeef1ea21e7f9ddc67c191d497abc8fc3ba2467857abbb68330" +checksum = "7b423f0e62bdd61734b67cd21ff50871dfaeb9cc74f869dcd6af974fbcb19936" dependencies = [ "futures", "futures-timer", @@ -5968,12 +5983,13 @@ dependencies = [ [[package]] name = "rstest_macros" -version = "0.19.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04a9df72cc1f67020b0d63ad9bfe4a323e459ea7eb68e03bd9824db49f9a4c25" +checksum = "c5e1711e7d14f74b12a58411c542185ef7fb7f2e7f8ee6e2940a883628522b42" dependencies = [ "cfg-if", "glob", + "proc-macro-crate 3.1.0", "proc-macro2", "quote", "regex", diff --git a/Cargo.toml b/Cargo.toml index 6583dbd2..17958668 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,14 +39,17 @@ identity_credential = { version = "1.3", default-features = false, features = [ identity_iota = { version = "1.3" } jsonwebtoken = "9.3" lazy_static = "1.4" +mime = { version = "0.3" } +once_cell = { version = "1.19" } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } -rstest = "0.19" +rstest = "0.22" serde = { version = "1.0", default-features = false, features = ["derive"] } serde_json = { version = "1.0" } serde_with = "3.7" serde_yaml = "0.9" thiserror = "1.0" tokio = { version = "1", features = ["full"] } +tower = { version = "0.4" } tower-http = { version = "0.5", features = ["cors", "trace"] } tracing = { version = "0.1" } tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] } diff --git a/agent_api_rest/Cargo.toml b/agent_api_rest/Cargo.toml index ef84cc78..73997c65 100644 --- a/agent_api_rest/Cargo.toml +++ b/agent_api_rest/Cargo.toml @@ -32,7 +32,7 @@ uuid.workspace = true agent_event_publisher_http = { path = "../agent_event_publisher_http", features = ["test_utils"] } agent_holder = { path = "../agent_holder", features = ["test_utils"] } agent_issuance = { path = "../agent_issuance", features = ["test_utils"] } -agent_secret_manager = { path = "../agent_secret_manager" } +agent_secret_manager = { path = "../agent_secret_manager", features = ["test_utils"] } agent_shared = { path = "../agent_shared", features = ["test_utils"] } agent_store = { path = "../agent_store" } agent_verification = { path = "../agent_verification", features = ["test_utils"] } @@ -40,13 +40,13 @@ agent_verification = { path = "../agent_verification", features = ["test_utils"] futures.workspace = true jsonwebtoken.workspace = true lazy_static.workspace = true -mime = { version = "0.3" } +mime.workspace = true oid4vc-core.workspace = true oid4vc-manager.workspace = true rstest.workspace = true serde_urlencoded = "0.7" serde_yaml.workspace = true serial_test = "3.0" -tower = { version = "0.4" } +tower.workspace = true tracing-test.workspace = true wiremock.workspace = true diff --git a/agent_api_rest/src/holder/holder/offers/accept.rs b/agent_api_rest/src/holder/holder/offers/accept.rs index f2626798..e6f1446e 100644 --- a/agent_api_rest/src/holder/holder/offers/accept.rs +++ b/agent_api_rest/src/holder/holder/offers/accept.rs @@ -28,17 +28,6 @@ pub(crate) async fn accept(State(state): State, Path(offer_id): Pat return StatusCode::INTERNAL_SERVER_ERROR.into_response(); } - let command = OfferCommand::SendTokenRequest { - offer_id: offer_id.clone(), - }; - - // Add the Credential Offer to the state. - if command_handler(&offer_id, &state.command.offer, command).await.is_err() { - // TODO: add better Error responses. This needs to be done properly in all endpoints once - // https://github.com/impierce/openid4vc/issues/78 is fixed. - return StatusCode::INTERNAL_SERVER_ERROR.into_response(); - } - let command = OfferCommand::SendCredentialRequest { offer_id: offer_id.clone(), }; diff --git a/agent_api_rest/src/issuance/credential_issuer/credential.rs b/agent_api_rest/src/issuance/credential_issuer/credential.rs index 81d1b4aa..39864600 100644 --- a/agent_api_rest/src/issuance/credential_issuer/credential.rs +++ b/agent_api_rest/src/issuance/credential_issuer/credential.rs @@ -48,7 +48,10 @@ pub(crate) async fn credential( Ok(Some(ServerConfigView { credential_issuer_metadata: Some(credential_issuer_metadata), authorization_server_metadata, - })) => (credential_issuer_metadata, Box::new(authorization_server_metadata)), + })) => ( + Box::new(credential_issuer_metadata), + Box::new(authorization_server_metadata), + ), _ => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), }; @@ -141,21 +144,17 @@ pub(crate) async fn credential( #[cfg(test)] mod tests { - use std::sync::Arc; - + use super::*; + use crate::issuance::credentials::tests::credentials; + use crate::issuance::router; + use crate::API_VERSION; use crate::{ issuance::{ credential_issuer::token::tests::token, credentials::CredentialsEndpointRequest, offers::tests::offers, }, tests::{BASE_URL, CREDENTIAL_CONFIGURATION_ID, OFFER_ID}, }; - - use super::*; - use crate::issuance::credentials::tests::credentials; - use crate::issuance::router; - use crate::API_VERSION; use agent_event_publisher_http::EventPublisherHttp; - use agent_issuance::services::test_utils::test_issuance_services; use agent_issuance::{offer::event::OfferEvent, startup_commands::startup_commands, state::initialize}; use agent_shared::config::{set_config, Events}; use agent_store::{in_memory, EventPublisher}; @@ -166,6 +165,7 @@ mod tests { }; use rstest::rstest; use serde_json::{json, Value}; + use std::sync::Arc; use tokio::sync::Mutex; use tower::ServiceExt; use wiremock::{ @@ -276,6 +276,8 @@ mod tests { #[case] is_self_signed: bool, #[case] delay: u64, ) { + use agent_secret_manager::service::Service; + let (external_server, issuance_event_publishers) = if with_external_server { let external_server = MockServer::start().await; @@ -296,7 +298,7 @@ mod tests { (None, Default::default()) }; - let issuance_state = in_memory::issuance_state(test_issuance_services(), issuance_event_publishers).await; + let issuance_state = in_memory::issuance_state(Service::default(), issuance_event_publishers).await; initialize(&issuance_state, startup_commands(BASE_URL.clone())).await; let mut app = router(issuance_state); diff --git a/agent_api_rest/src/issuance/credential_issuer/token.rs b/agent_api_rest/src/issuance/credential_issuer/token.rs index 34f9f903..72728ad5 100644 --- a/agent_api_rest/src/issuance/credential_issuer/token.rs +++ b/agent_api_rest/src/issuance/credential_issuer/token.rs @@ -60,15 +60,13 @@ pub(crate) async fn token( #[cfg(test)] pub mod tests { + use super::*; use crate::{ issuance::{credentials::tests::credentials, offers::tests::offers, router}, tests::BASE_URL, }; - - use super::*; - use agent_issuance::{ - services::test_utils::test_issuance_services, startup_commands::startup_commands, state::initialize, - }; + use agent_issuance::{startup_commands::startup_commands, state::initialize}; + use agent_secret_manager::service::Service; use agent_store::in_memory; use axum::{ body::Body, @@ -76,7 +74,7 @@ pub mod tests { Router, }; use oid4vci::token_response::TokenResponse; - use tower::Service; + use tower::Service as _; pub async fn token(app: &mut Router, pre_authorized_code: String) -> String { let response = app @@ -110,7 +108,7 @@ pub mod tests { #[tokio::test] async fn test_token_endpoint() { - let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; + let issuance_state = in_memory::issuance_state(Service::default(), Default::default()).await; initialize(&issuance_state, startup_commands(BASE_URL.clone())).await; let mut app = router(issuance_state); diff --git a/agent_api_rest/src/issuance/credential_issuer/well_known/oauth_authorization_server.rs b/agent_api_rest/src/issuance/credential_issuer/well_known/oauth_authorization_server.rs index 723c6e25..73ab36c3 100644 --- a/agent_api_rest/src/issuance/credential_issuer/well_known/oauth_authorization_server.rs +++ b/agent_api_rest/src/issuance/credential_issuer/well_known/oauth_authorization_server.rs @@ -23,12 +23,10 @@ pub(crate) async fn oauth_authorization_server(State(state): State AuthorizationServerMetadata { let response = app @@ -71,7 +69,7 @@ mod tests { #[tokio::test] async fn test_oauth_authorization_server_endpoint() { - let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; + let issuance_state = in_memory::issuance_state(Service::default(), Default::default()).await; initialize(&issuance_state, startup_commands(BASE_URL.clone())).await; let mut app = router(issuance_state); diff --git a/agent_api_rest/src/issuance/credential_issuer/well_known/openid_credential_issuer.rs b/agent_api_rest/src/issuance/credential_issuer/well_known/openid_credential_issuer.rs index bfc0f9bb..92a9c3dd 100644 --- a/agent_api_rest/src/issuance/credential_issuer/well_known/openid_credential_issuer.rs +++ b/agent_api_rest/src/issuance/credential_issuer/well_known/openid_credential_issuer.rs @@ -23,14 +23,10 @@ pub(crate) async fn openid_credential_issuer(State(state): State) #[cfg(test)] mod tests { - use std::collections::HashMap; - - use crate::{issuance::router, tests::BASE_URL}; - use super::*; - use agent_issuance::{ - services::test_utils::test_issuance_services, startup_commands::startup_commands, state::initialize, - }; + use crate::{issuance::router, tests::BASE_URL}; + use agent_issuance::{startup_commands::startup_commands, state::initialize}; + use agent_secret_manager::service::Service; use agent_shared::UrlAppendHelpers; use agent_store::in_memory; use axum::{ @@ -51,7 +47,8 @@ mod tests { ProofType, }; use serde_json::json; - use tower::Service; + use std::collections::HashMap; + use tower::Service as _; pub async fn openid_credential_issuer(app: &mut Router) -> CredentialIssuerMetadata { let response = app @@ -132,7 +129,7 @@ mod tests { #[tokio::test] async fn test_openid_credential_issuer_endpoint() { - let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; + let issuance_state = in_memory::issuance_state(Service::default(), Default::default()).await; initialize(&issuance_state, startup_commands(BASE_URL.clone())).await; let mut app = router(issuance_state); diff --git a/agent_api_rest/src/issuance/credentials.rs b/agent_api_rest/src/issuance/credentials.rs index 71eb920e..f8ea8a11 100644 --- a/agent_api_rest/src/issuance/credentials.rs +++ b/agent_api_rest/src/issuance/credentials.rs @@ -111,7 +111,7 @@ pub(crate) async fn credentials( Ok(Some(ServerConfigView { credential_issuer_metadata: Some(credential_issuer_metadata), .. - })) => credential_issuer_metadata, + })) => Box::new(credential_issuer_metadata), _ => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), }; @@ -166,8 +166,8 @@ pub mod tests { use crate::issuance::router; use crate::tests::{BASE_URL, CREDENTIAL_CONFIGURATION_ID, OFFER_ID}; use crate::API_VERSION; - use agent_issuance::services::test_utils::test_issuance_services; use agent_issuance::{startup_commands::startup_commands, state::initialize}; + use agent_secret_manager::service::Service; use agent_store::in_memory; use axum::{ body::Body, @@ -176,7 +176,7 @@ pub mod tests { }; use lazy_static::lazy_static; use serde_json::json; - use tower::Service; + use tower::Service as _; lazy_static! { pub static ref CREDENTIAL_SUBJECT: serde_json::Value = json!({ @@ -260,7 +260,7 @@ pub mod tests { #[tokio::test] #[tracing_test::traced_test] async fn test_credentials_endpoint() { - let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; + let issuance_state = in_memory::issuance_state(Service::default(), Default::default()).await; initialize(&issuance_state, startup_commands(BASE_URL.clone())).await; let mut app = router(issuance_state); diff --git a/agent_api_rest/src/issuance/offers/mod.rs b/agent_api_rest/src/issuance/offers/mod.rs index b6bd1dfe..06900dfb 100644 --- a/agent_api_rest/src/issuance/offers/mod.rs +++ b/agent_api_rest/src/issuance/offers/mod.rs @@ -35,7 +35,7 @@ pub(crate) async fn offers(State(state): State, Json(payload): Js Ok(Some(ServerConfigView { credential_issuer_metadata: Some(credential_issuer_metadata), .. - })) => credential_issuer_metadata, + })) => Box::new(credential_issuer_metadata), _ => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), }; @@ -83,18 +83,14 @@ pub(crate) async fn offers(State(state): State, Json(payload): Js #[cfg(test)] pub mod tests { - use std::str::FromStr; - + use super::*; + use crate::API_VERSION; use crate::{ issuance::{credentials::tests::credentials, router}, tests::{BASE_URL, OFFER_ID}, }; - - use super::*; - use crate::API_VERSION; - use agent_issuance::{ - services::test_utils::test_issuance_services, startup_commands::startup_commands, state::initialize, - }; + use agent_issuance::{startup_commands::startup_commands, state::initialize}; + use agent_secret_manager::service::Service; use agent_store::in_memory; use axum::{ body::Body, @@ -103,7 +99,8 @@ pub mod tests { }; use oid4vci::credential_offer::{CredentialOffer, CredentialOfferParameters, Grants, PreAuthorizedCode}; use serde_json::json; - use tower::Service; + use std::str::FromStr; + use tower::Service as _; pub async fn offers(app: &mut Router) -> String { let response = app @@ -156,7 +153,7 @@ pub mod tests { #[tokio::test] #[tracing_test::traced_test] async fn test_offers_endpoint() { - let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; + let issuance_state = in_memory::issuance_state(Service::default(), Default::default()).await; initialize(&issuance_state, startup_commands(BASE_URL.clone())).await; let mut app = router(issuance_state); diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index d21a3a98..2fa9e367 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -1,6 +1,6 @@ -mod holder; -mod issuance; -mod verification; +pub mod holder; +pub mod issuance; +pub mod verification; use agent_holder::state::HolderState; use agent_issuance::state::IssuanceState; @@ -95,10 +95,8 @@ fn get_base_path() -> Result { #[cfg(test)] mod tests { - use std::collections::HashMap; - use super::*; - use agent_issuance::services::test_utils::test_issuance_services; + use agent_secret_manager::service::Service; use agent_store::in_memory; use axum::routing::post; use oid4vci::credential_issuer::{ @@ -106,6 +104,7 @@ mod tests { credential_issuer_metadata::CredentialIssuerMetadata, }; use serde_json::json; + use std::collections::HashMap; pub const CREDENTIAL_CONFIGURATION_ID: &str = "badge"; pub const OFFER_ID: &str = "00000000-0000-0000-0000-000000000000"; @@ -152,7 +151,7 @@ mod tests { #[tokio::test] #[should_panic] async fn test_base_path_routes() { - let issuance_state = in_memory::issuance_state(test_issuance_services(), Default::default()).await; + let issuance_state = in_memory::issuance_state(Service::default(), Default::default()).await; std::env::set_var("UNICORE__BASE_PATH", "unicore"); let router = app(ApplicationState { issuance_state: Some(issuance_state), diff --git a/agent_api_rest/src/verification/authorization_requests.rs b/agent_api_rest/src/verification/authorization_requests.rs index 2c75c736..e934bc3f 100644 --- a/agent_api_rest/src/verification/authorization_requests.rs +++ b/agent_api_rest/src/verification/authorization_requests.rs @@ -139,15 +139,15 @@ pub(crate) async fn authorization_requests( pub mod tests { use super::*; use crate::verification::router; + use agent_secret_manager::service::Service; use agent_store::in_memory; - use agent_verification::services::test_utils::test_verification_services; use axum::{ body::Body, http::{self, Request}, Router, }; use rstest::rstest; - use tower::Service; + use tower::Service as _; pub async fn authorization_requests(app: &mut Router, by_value: bool) -> String { let request_body = AuthorizationRequestsEndpointRequest { @@ -220,7 +220,7 @@ pub mod tests { #[tokio::test] #[tracing_test::traced_test] async fn test_authorization_requests_endpoint(#[case] by_value: bool) { - let verification_state = in_memory::verification_state(test_verification_services(), Default::default()).await; + let verification_state = in_memory::verification_state(Service::default(), Default::default()).await; let mut app = router(verification_state); diff --git a/agent_api_rest/src/verification/relying_party/redirect.rs b/agent_api_rest/src/verification/relying_party/redirect.rs index b3850b3e..4f8061b0 100644 --- a/agent_api_rest/src/verification/relying_party/redirect.rs +++ b/agent_api_rest/src/verification/relying_party/redirect.rs @@ -61,10 +61,9 @@ pub mod tests { authorization_requests::tests::authorization_requests, relying_party::request::tests::request, router, }; use agent_event_publisher_http::EventPublisherHttp; - use agent_secret_manager::{secret_manager, subject::Subject}; + use agent_secret_manager::{secret_manager, service::Service, subject::Subject}; use agent_shared::config::{set_config, Events}; use agent_store::{in_memory, EventPublisher}; - use agent_verification::services::test_utils::test_verification_services; use axum::{ body::Body, http::{self, Request}, @@ -79,7 +78,7 @@ pub mod tests { }; use oid4vc_manager::ProviderManager; use siopv2::{authorization_request::ClientMetadataParameters, siopv2::SIOPv2}; - use tower::Service; + use tower::Service as _; use wiremock::{ matchers::{method, path}, Mock, MockServer, ResponseTemplate, @@ -160,7 +159,7 @@ pub mod tests { let event_publishers = vec![Box::new(EventPublisherHttp::load().unwrap()) as Box]; - let verification_state = in_memory::verification_state(test_verification_services(), event_publishers).await; + let verification_state = in_memory::verification_state(Service::default(), event_publishers).await; let mut app = router(verification_state); diff --git a/agent_api_rest/src/verification/relying_party/request.rs b/agent_api_rest/src/verification/relying_party/request.rs index 5bf2d2f1..ff73e918 100644 --- a/agent_api_rest/src/verification/relying_party/request.rs +++ b/agent_api_rest/src/verification/relying_party/request.rs @@ -34,14 +34,14 @@ pub(crate) async fn request( pub mod tests { use super::*; use crate::verification::{authorization_requests::tests::authorization_requests, router}; + use agent_secret_manager::service::Service; use agent_store::in_memory; - use agent_verification::services::test_utils::test_verification_services; use axum::{ body::Body, http::{self, Request}, Router, }; - use tower::Service; + use tower::Service as _; pub async fn request(app: &mut Router, state: String) { let response = app @@ -69,7 +69,7 @@ pub mod tests { #[tokio::test] #[tracing_test::traced_test] async fn test_request_endpoint() { - let verification_state = in_memory::verification_state(test_verification_services(), Default::default()).await; + let verification_state = in_memory::verification_state(Service::default(), Default::default()).await; let mut app = router(verification_state); diff --git a/agent_application/src/main.rs b/agent_application/src/main.rs index 9489d46b..7b9599c3 100644 --- a/agent_application/src/main.rs +++ b/agent_application/src/main.rs @@ -4,7 +4,7 @@ use agent_api_rest::{app, ApplicationState}; use agent_event_publisher_http::EventPublisherHttp; use agent_holder::services::HolderServices; use agent_issuance::{services::IssuanceServices, startup_commands::startup_commands, state::initialize}; -use agent_secret_manager::{secret_manager, subject::Subject}; +use agent_secret_manager::{secret_manager, service::Service as _, subject::Subject}; use agent_shared::{ config::{config, LogFormat, SupportedDidMethod, ToggleOptions}, domain_linkage::create_did_configuration_resource, diff --git a/agent_holder/Cargo.toml b/agent_holder/Cargo.toml index 8395e0e2..0601c04f 100644 --- a/agent_holder/Cargo.toml +++ b/agent_holder/Cargo.toml @@ -28,22 +28,30 @@ tracing.workspace = true url.workspace = true uuid.workspace = true +# `test_utils` dependencies +rstest = { workspace = true, optional = true } + [dev-dependencies] agent_api_rest = { path = "../agent_api_rest" } agent_holder = { path = ".", features = ["test_utils"] } agent_issuance = { path = "../agent_issuance", features = ["test_utils"] } +agent_secret_manager = { path = "../agent_secret_manager", features = ["test_utils"] } agent_shared = { path = "../agent_shared", features = ["test_utils"] } agent_store = { path = "../agent_store" } -agent_verification = { path = "../agent_verification", features = ["test_utils"] } +# agent_verification = { path = "../agent_verification", features = ["test_utils"] } -# axum-test = "15.6" +axum.workspace = true did_manager.workspace = true lazy_static.workspace = true +mime.workspace = true +names = { version = "0.14", default-features = false } +reqwest.workspace = true +rand = "0.8" serial_test = "3.0" tokio.workspace = true +tower.workspace = true tracing-test.workspace = true async-std = { version = "1.5", features = ["attributes", "tokio1"] } -rstest.workspace = true [features] -test_utils = [] +test_utils = ["dep:rstest"] diff --git a/agent_holder/src/credential/aggregate.rs b/agent_holder/src/credential/aggregate.rs index 9c65c71b..97df95a6 100644 --- a/agent_holder/src/credential/aggregate.rs +++ b/agent_holder/src/credential/aggregate.rs @@ -1,35 +1,13 @@ -use agent_shared::config::{config, get_preferred_did_method, get_preferred_signing_algorithm}; +use crate::credential::command::CredentialCommand; +use crate::credential::error::CredentialError::{self}; +use crate::credential::event::CredentialEvent; +use crate::services::HolderServices; use async_trait::async_trait; use cqrs_es::Aggregate; use derivative::Derivative; -use identity_core::convert::FromJson; -use identity_credential::credential::{ - Credential as W3CVerifiableCredential, CredentialBuilder as W3CVerifiableCredentialBuilder, Issuer, -}; -use jsonwebtoken::Header; -use oid4vc_core::jwt; -use oid4vci::credential_format_profiles::w3c_verifiable_credentials::jwt_vc_json::{ - CredentialDefinition, JwtVcJson, JwtVcJsonParameters, -}; -use oid4vci::credential_format_profiles::{CredentialFormats, Parameters}; -use oid4vci::credential_issuer::credential_configurations_supported::CredentialConfigurationsSupportedObject; -use oid4vci::credential_response::CredentialResponseType; -use oid4vci::VerifiableCredentialJwt; use serde::{Deserialize, Serialize}; -use serde_json::json; use std::sync::Arc; use tracing::info; -use types_ob_v3::prelude::{ - AchievementCredential, AchievementCredentialBuilder, AchievementCredentialType, AchievementSubject, Profile, - ProfileBuilder, -}; - -use crate::credential::command::CredentialCommand; -use crate::credential::error::CredentialError::{self}; -use crate::credential::event::CredentialEvent; -use crate::services::HolderServices; - -use super::entity::Data; #[derive(Debug, Clone, Serialize, Deserialize, Default, Derivative)] #[derivative(PartialEq)] @@ -50,9 +28,12 @@ impl Aggregate for Credential { "credential".to_string() } - async fn handle(&self, command: Self::Command, services: &Self::Services) -> Result, Self::Error> { + async fn handle( + &self, + command: Self::Command, + _services: &Self::Services, + ) -> Result, Self::Error> { use CredentialCommand::*; - use CredentialError::*; use CredentialEvent::*; info!("Handling command: {:?}", command); @@ -89,218 +70,46 @@ impl Aggregate for Credential { } } -// #[cfg(test)] -// pub mod credential_tests { -// use std::collections::HashMap; - -// use super::*; - -// use jsonwebtoken::Algorithm; -// use lazy_static::lazy_static; -// use oid4vci::proof::KeyProofMetadata; -// use oid4vci::ProofType; -// use rstest::rstest; -// use serde_json::json; - -// use cqrs_es::test::TestFramework; - -// use crate::credential::aggregate::Credential; -// use crate::credential::event::CredentialEvent; -// use crate::offer::aggregate::tests::SUBJECT_KEY_DID; -// use crate::services::test_utils::test_issuance_services; - -// type CredentialTestFramework = TestFramework; - -// #[rstest] -// #[case::openbadges( -// OPENBADGE_CREDENTIAL_SUBJECT.clone(), -// OPENBADGE_CREDENTIAL_CONFIGURATION.clone(), -// UNSIGNED_OPENBADGE_CREDENTIAL.clone() -// )] -// #[case::w3c_vc( -// W3C_VC_CREDENTIAL_SUBJECT.clone(), -// W3C_VC_CREDENTIAL_CONFIGURATION.clone(), -// UNSIGNED_W3C_VC_CREDENTIAL.clone() -// )] -// #[serial_test::serial] -// fn test_create_unsigned_credential( -// #[case] credential_subject: serde_json::Value, -// #[case] credential_configuration: CredentialConfigurationsSupportedObject, -// #[case] unsigned_credential: serde_json::Value, -// ) { -// CredentialTestFramework::with(test_issuance_services()) -// .given_no_previous_events() -// .when(CredentialCommand::CreateUnsignedCredential { -// data: Data { -// raw: credential_subject, -// }, -// credential_configuration: credential_configuration.clone(), -// }) -// .then_expect_events(vec![CredentialEvent::UnsignedCredentialCreated { -// data: Data { -// raw: unsigned_credential, -// }, -// credential_configuration, -// }]) -// } - -// #[rstest] -// #[case::openbadges( -// UNSIGNED_OPENBADGE_CREDENTIAL.clone(), -// OPENBADGE_CREDENTIAL_CONFIGURATION.clone(), -// OPENBADGE_VERIFIABLE_CREDENTIAL_JWT.to_string(), -// )] -// #[case::w3c_vc( -// UNSIGNED_W3C_VC_CREDENTIAL.clone(), -// W3C_VC_CREDENTIAL_CONFIGURATION.clone(), -// W3C_VC_VERIFIABLE_CREDENTIAL_JWT.to_string(), -// )] -// #[serial_test::serial] -// async fn test_sign_credential( -// #[case] unsigned_credential: serde_json::Value, -// #[case] credential_configuration: CredentialConfigurationsSupportedObject, -// #[case] verifiable_credential_jwt: String, -// ) { -// CredentialTestFramework::with(test_issuance_services()) -// .given(vec![CredentialEvent::UnsignedCredentialCreated { -// data: Data { -// raw: unsigned_credential, -// }, -// credential_configuration, -// }]) -// .when(CredentialCommand::SignCredential { -// subject_id: SUBJECT_KEY_DID.identifier("did:key", Algorithm::EdDSA).await.unwrap(), -// overwrite: false, -// }) -// .then_expect_events(vec![CredentialEvent::CredentialSigned { -// signed_credential: json!(verifiable_credential_jwt), -// }]) -// } - -// lazy_static! { -// static ref OPENBADGE_CREDENTIAL_CONFIGURATION: CredentialConfigurationsSupportedObject = -// CredentialConfigurationsSupportedObject { -// credential_format: CredentialFormats::JwtVcJson(Parameters { -// parameters: ( -// CredentialDefinition { -// type_: vec!["VerifiableCredential".to_string(), "OpenBadgeCredential".to_string()], -// credential_subject: Default::default(), -// }, -// None, -// ) -// .into(), -// }), -// cryptographic_binding_methods_supported: vec![ -// "did:key".to_string(), -// "did:key".to_string(), -// "did:iota:rms".to_string(), -// "did:jwk".to_string(), -// ], -// credential_signing_alg_values_supported: vec!["EdDSA".to_string()], -// proof_types_supported: HashMap::from_iter(vec![( -// ProofType::Jwt, -// KeyProofMetadata { -// proof_signing_alg_values_supported: vec![Algorithm::EdDSA], -// }, -// )]), -// display: vec![json!({ -// "name": "Teamwork Badge", -// "logo": { -// "url": "https://example.com/logo.png" -// } -// })], -// ..Default::default() -// }; -// static ref W3C_VC_CREDENTIAL_CONFIGURATION: CredentialConfigurationsSupportedObject = -// CredentialConfigurationsSupportedObject { -// credential_format: CredentialFormats::JwtVcJson(Parameters { -// parameters: ( -// CredentialDefinition { -// type_: vec!["VerifiableCredential".to_string()], -// credential_subject: Default::default(), -// }, -// None, -// ) -// .into(), -// }), -// cryptographic_binding_methods_supported: vec![ -// "did:key".to_string(), -// "did:key".to_string(), -// "did:iota:rms".to_string(), -// "did:jwk".to_string(), -// ], -// credential_signing_alg_values_supported: vec!["EdDSA".to_string()], -// proof_types_supported: HashMap::from_iter(vec![( -// ProofType::Jwt, -// KeyProofMetadata { -// proof_signing_alg_values_supported: vec![Algorithm::EdDSA], -// }, -// )]), -// display: vec![json!({ -// "name": "Master Degree", -// "logo": { -// "url": "https://example.com/logo.png" -// } -// })], -// ..Default::default() -// }; -// static ref OPENBADGE_CREDENTIAL_SUBJECT: serde_json::Value = json!( -// { -// "credentialSubject": { -// "type": [ "AchievementSubject" ], -// "achievement": { -// "id": "https://example.com/achievements/21st-century-skills/teamwork", -// "type": "Achievement", -// "criteria": { -// "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." -// }, -// "description": "This badge recognizes the development of the capacity to collaborate within a group environment.", -// "name": "Teamwork" -// } -// } -// } -// ); -// static ref W3C_VC_CREDENTIAL_SUBJECT: serde_json::Value = json!( -// { -// "credentialSubject": { -// "first_name": "Ferris", -// "last_name": "Rustacean", -// "degree": { -// "type": "MasterDegree", -// "name": "Master of Oceanography" -// } -// } -// } -// ); -// static ref UNSIGNED_OPENBADGE_CREDENTIAL: serde_json::Value = json!({ -// "@context": [ -// "https://www.w3.org/2018/credentials/v1", -// "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json" -// ], -// "id": "http://example.com/credentials/3527", -// "type": ["VerifiableCredential", "OpenBadgeCredential"], -// "issuer": { -// "id": "https://my-domain.example.org", -// "type": "Profile", -// "name": "UniCore" -// }, -// "issuanceDate": "2010-01-01T00:00:00Z", -// "name": "Teamwork Badge", -// "credentialSubject": OPENBADGE_CREDENTIAL_SUBJECT["credentialSubject"].clone(), -// }); -// static ref UNSIGNED_W3C_VC_CREDENTIAL: serde_json::Value = json!({ -// "@context": "https://www.w3.org/2018/credentials/v1", -// "type": [ "VerifiableCredential" ], -// "credentialSubject": W3C_VC_CREDENTIAL_SUBJECT["credentialSubject"].clone(), -// "issuer": { -// "id": "https://my-domain.example.org/", -// "name": "UniCore" -// }, -// "issuanceDate": "2010-01-01T00:00:00Z" -// }); -// } +#[cfg(test)] +pub mod credential_tests { + use super::test_utils::*; + use super::*; + use crate::credential::aggregate::Credential; + use crate::credential::event::CredentialEvent; + use crate::offer::aggregate::test_utils::offer_id; + use agent_issuance::credential::aggregate::test_utils::OPENBADGE_VERIFIABLE_CREDENTIAL_JWT; + use agent_secret_manager::service::Service; + use cqrs_es::test::TestFramework; + use rstest::rstest; + use serde_json::json; + + type CredentialTestFramework = TestFramework; + + #[rstest] + #[serial_test::serial] + fn test_add_credential(credential_id: String, offer_id: String) { + CredentialTestFramework::with(Service::default()) + .given_no_previous_events() + .when(CredentialCommand::AddCredential { + credential_id: credential_id.clone(), + offer_id: offer_id.clone(), + credential: json!(OPENBADGE_VERIFIABLE_CREDENTIAL_JWT), + }) + .then_expect_events(vec![CredentialEvent::CredentialAdded { + credential_id, + offer_id, + credential: json!(OPENBADGE_VERIFIABLE_CREDENTIAL_JWT), + }]) + } +} -// pub const OPENBADGE_VERIFIABLE_CREDENTIAL_JWT: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjIuanNvbiJdLCJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9jcmVkZW50aWFscy8zNTI3IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIk9wZW5CYWRnZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiIsIm5hbWUiOiJUZWFtd29yayBCYWRnZSIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJ0eXBlIjpbIkFjaGlldmVtZW50U3ViamVjdCJdLCJhY2hpZXZlbWVudCI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vYWNoaWV2ZW1lbnRzLzIxc3QtY2VudHVyeS1za2lsbHMvdGVhbXdvcmsiLCJ0eXBlIjoiQWNoaWV2ZW1lbnQiLCJjcml0ZXJpYSI6eyJuYXJyYXRpdmUiOiJUZWFtIG1lbWJlcnMgYXJlIG5vbWluYXRlZCBmb3IgdGhpcyBiYWRnZSBieSB0aGVpciBwZWVycyBhbmQgcmVjb2duaXplZCB1cG9uIHJldmlldyBieSBFeGFtcGxlIENvcnAgbWFuYWdlbWVudC4ifSwiZGVzY3JpcHRpb24iOiJUaGlzIGJhZGdlIHJlY29nbml6ZXMgdGhlIGRldmVsb3BtZW50IG9mIHRoZSBjYXBhY2l0eSB0byBjb2xsYWJvcmF0ZSB3aXRoaW4gYSBncm91cCBlbnZpcm9ubWVudC4iLCJuYW1lIjoiVGVhbXdvcmsifX19fQ.SkC7IvpBGB9e98eobnE9qcLjs-yoZup3cieBla3DRTlcRezXEDPv4YRoUgffho9LJ0rkmfFPsPwb-owXMWyPAA"; +#[cfg(feature = "test_utils")] +pub mod test_utils { + use agent_shared::generate_random_string; + use rstest::*; -// pub const W3C_VC_VERIFIABLE_CREDENTIAL_JWT: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJmaXJzdF9uYW1lIjoiRmVycmlzIiwibGFzdF9uYW1lIjoiUnVzdGFjZWFuIiwiZGVncmVlIjp7InR5cGUiOiJNYXN0ZXJEZWdyZWUiLCJuYW1lIjoiTWFzdGVyIG9mIE9jZWFub2dyYXBoeSJ9fSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiJ9fQ.MUDBbPJfXe0G9sjVTF3RuR6ukRM0d4N57iMGNFcIKMFPIEdig12v-YFB0qfnSghGcQo8hUw3jzxZXTSJATEgBg"; -// } + #[fixture] + pub fn credential_id() -> String { + generate_random_string() + } +} diff --git a/agent_holder/src/credential/command.rs b/agent_holder/src/credential/command.rs index 7385f003..af839527 100644 --- a/agent_holder/src/credential/command.rs +++ b/agent_holder/src/credential/command.rs @@ -1,14 +1,5 @@ -use oid4vci::{ - credential_issuer::{ - credential_configurations_supported::CredentialConfigurationsSupportedObject, - credential_issuer_metadata::CredentialIssuerMetadata, - }, - token_response::TokenResponse, -}; use serde::Deserialize; -use super::entity::Data; - #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum CredentialCommand { diff --git a/agent_holder/src/credential/queries/all_credentials.rs b/agent_holder/src/credential/queries/all_credentials.rs index 4f9b78fd..dbdb764c 100644 --- a/agent_holder/src/credential/queries/all_credentials.rs +++ b/agent_holder/src/credential/queries/all_credentials.rs @@ -87,7 +87,7 @@ impl View for AllCredentialsView { // Get the entry for the aggregate_id .entry(event.aggregate_id.clone()) // or insert a new one if it doesn't exist - .or_insert_with(Default::default) + .or_default() // update the view with the event .update(event); } diff --git a/agent_holder/src/offer/aggregate.rs b/agent_holder/src/offer/aggregate.rs index aae79a9d..6edb89fc 100644 --- a/agent_holder/src/offer/aggregate.rs +++ b/agent_holder/src/offer/aggregate.rs @@ -1,12 +1,12 @@ -use agent_shared::generate_random_string; +use crate::offer::command::OfferCommand; +use crate::offer::error::OfferError; +use crate::offer::event::OfferEvent; +use crate::services::HolderServices; use async_trait::async_trait; use cqrs_es::Aggregate; -use oid4vc_core::Validator; use oid4vci::credential_issuer::credential_configurations_supported::CredentialConfigurationsSupportedObject; -use oid4vci::credential_issuer::CredentialIssuer; -use oid4vci::credential_offer::{CredentialOffer, CredentialOfferParameters, Grants, PreAuthorizedCode}; -use oid4vci::credential_request::CredentialRequest; -use oid4vci::credential_response::{CredentialResponse, CredentialResponseType}; +use oid4vci::credential_offer::{CredentialOffer, CredentialOfferParameters, Grants}; +use oid4vci::credential_response::CredentialResponseType; use oid4vci::token_request::TokenRequest; use oid4vci::token_response::TokenResponse; use serde::{Deserialize, Serialize}; @@ -14,11 +14,6 @@ use std::collections::HashMap; use std::sync::Arc; use tracing::info; -use crate::offer::command::OfferCommand; -use crate::offer::error::OfferError::{self, *}; -use crate::offer::event::OfferEvent; -use crate::services::HolderServices; - #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] pub enum Status { #[default] @@ -38,13 +33,6 @@ pub struct Offer { // `CredentialResponseReceived` event and then trigger the `CredentialCommand::AddCredential` command. We can do // this once we have a mechanism implemented that can both listen to events as well as trigger commands. pub credentials: Vec, - // pub subject_id: Option, - // pub credential_ids: Vec, - // pub form_url_encoded_credential_offer: String, - // pub pre_authorized_code: String, - // pub token_response: Option, - // pub access_token: String, - // pub credential_response: Option, } #[async_trait] @@ -99,15 +87,11 @@ impl Aggregate for Offer { Ok(vec![CredentialOfferReceived { offer_id, - credential_offer, + credential_offer: Box::new(credential_offer), credential_configurations, }]) } - AcceptCredentialOffer { offer_id } => Ok(vec![CredentialOfferAccepted { - offer_id, - status: Status::Accepted, - }]), - SendTokenRequest { offer_id } => { + AcceptCredentialOffer { offer_id } => { let wallet = &services.wallet; let credential_issuer_url = self.credential_offer.as_ref().unwrap().credential_issuer.clone(); @@ -139,10 +123,16 @@ impl Aggregate for Offer { info!("token_response: {:?}", token_response); - Ok(vec![TokenResponseReceived { - offer_id, - token_response, - }]) + Ok(vec![ + CredentialOfferAccepted { + offer_id: offer_id.clone(), + status: Status::Accepted, + }, + TokenResponseReceived { + offer_id, + token_response, + }, + ]) } SendCredentialRequest { offer_id } => { let wallet = &services.wallet; @@ -221,7 +211,7 @@ impl Aggregate for Offer { credential_configurations, .. } => { - self.credential_offer.replace(credential_offer); + self.credential_offer.replace(*credential_offer); self.credential_configurations.replace(credential_configurations); } CredentialOfferAccepted { status, .. } => { @@ -243,308 +233,227 @@ impl Aggregate for Offer { } } -// #[cfg(test)] -// pub mod tests { -// use super::*; - -// use cqrs_es::test::TestFramework; -// use jsonwebtoken::Algorithm; -// use lazy_static::lazy_static; -// use oid4vci::{ -// credential_format_profiles::{ -// w3c_verifiable_credentials::jwt_vc_json::CredentialDefinition, CredentialFormats, Parameters, -// }, -// credential_request::CredentialRequest, -// KeyProofType, ProofType, -// }; -// use rstest::rstest; -// use serde_json::json; -// use std::{collections::VecDeque, sync::Mutex}; - -// use crate::{ -// credential::aggregate::credential_tests::OPENBADGE_VERIFIABLE_CREDENTIAL_JWT, -// server_config::aggregate::server_config_tests::{AUTHORIZATION_SERVER_METADATA, CREDENTIAL_ISSUER_METADATA}, -// services::test_utils::test_issuance_services, -// }; - -// type OfferTestFramework = TestFramework; - -// #[test] -// #[serial_test::serial] -// fn test_create_offer() { -// *PRE_AUTHORIZED_CODES.lock().unwrap() = vec![generate_random_string()].into(); -// *ACCESS_TOKENS.lock().unwrap() = vec![generate_random_string()].into(); -// *C_NONCES.lock().unwrap() = vec![generate_random_string()].into(); - -// let subject = test_subject(); -// OfferTestFramework::with(test_issuance_services()) -// .given_no_previous_events() -// .when(OfferCommand::CreateCredentialOffer { -// offer_id: Default::default(), -// }) -// .then_expect_events(vec![OfferEvent::CredentialOfferCreated { -// offer_id: Default::default(), -// pre_authorized_code: subject.pre_authorized_code, -// access_token: subject.access_token, -// }]); -// } - -// #[test] -// #[serial_test::serial] -// fn test_add_credential() { -// *PRE_AUTHORIZED_CODES.lock().unwrap() = vec![generate_random_string()].into(); -// *ACCESS_TOKENS.lock().unwrap() = vec![generate_random_string()].into(); -// *C_NONCES.lock().unwrap() = vec![generate_random_string()].into(); - -// let subject = test_subject(); -// OfferTestFramework::with(test_issuance_services()) -// .given(vec![OfferEvent::CredentialOfferCreated { -// offer_id: Default::default(), -// pre_authorized_code: subject.pre_authorized_code.clone(), -// access_token: subject.access_token.clone(), -// }]) -// .when(OfferCommand::AddCredentials { -// offer_id: Default::default(), -// credential_ids: vec!["credential-id".to_string()], -// }) -// .then_expect_events(vec![OfferEvent::CredentialsAdded { -// offer_id: Default::default(), -// credential_ids: vec!["credential-id".to_string()], -// }]); -// } - -// #[test] -// #[serial_test::serial] -// fn test_create_credential_offer() { -// *PRE_AUTHORIZED_CODES.lock().unwrap() = vec![generate_random_string()].into(); -// *ACCESS_TOKENS.lock().unwrap() = vec![generate_random_string()].into(); -// *C_NONCES.lock().unwrap() = vec![generate_random_string()].into(); - -// let subject = test_subject(); -// OfferTestFramework::with(test_issuance_services()) -// .given(vec![ -// OfferEvent::CredentialOfferCreated { -// offer_id: Default::default(), -// pre_authorized_code: subject.pre_authorized_code, -// access_token: subject.access_token, -// }, -// OfferEvent::CredentialsAdded { -// offer_id: Default::default(), -// credential_ids: vec!["credential-id".to_string()], -// }, -// ]) -// .when(OfferCommand::CreateFormUrlEncodedCredentialOffer { -// offer_id: Default::default(), -// credential_issuer_metadata: CREDENTIAL_ISSUER_METADATA.clone(), -// }) -// .then_expect_events(vec![OfferEvent::FormUrlEncodedCredentialOfferCreated { -// offer_id: Default::default(), -// form_url_encoded_credential_offer: subject.form_url_encoded_credential_offer, -// }]); -// } - -// #[test] -// #[serial_test::serial] -// fn test_create_token_response() { -// *PRE_AUTHORIZED_CODES.lock().unwrap() = vec![generate_random_string()].into(); -// *ACCESS_TOKENS.lock().unwrap() = vec![generate_random_string()].into(); -// *C_NONCES.lock().unwrap() = vec![generate_random_string()].into(); - -// let subject = test_subject(); -// OfferTestFramework::with(test_issuance_services()) -// .given(vec![ -// OfferEvent::CredentialOfferCreated { -// offer_id: Default::default(), -// pre_authorized_code: subject.pre_authorized_code.clone(), -// access_token: subject.access_token.clone(), -// }, -// OfferEvent::CredentialsAdded { -// offer_id: Default::default(), -// credential_ids: vec!["credential-id".to_string()], -// }, -// OfferEvent::FormUrlEncodedCredentialOfferCreated { -// offer_id: Default::default(), -// form_url_encoded_credential_offer: subject.form_url_encoded_credential_offer.clone(), -// }, -// ]) -// .when(OfferCommand::CreateTokenResponse { -// offer_id: Default::default(), -// token_request: token_request(subject.clone()), -// }) -// .then_expect_events(vec![OfferEvent::TokenResponseCreated { -// offer_id: Default::default(), -// token_response: token_response(subject), -// }]); -// } - -// #[rstest] -// #[serial_test::serial] -// async fn test_verify_credential_response() { -// *PRE_AUTHORIZED_CODES.lock().unwrap() = vec![generate_random_string()].into(); -// *ACCESS_TOKENS.lock().unwrap() = vec![generate_random_string()].into(); -// *C_NONCES.lock().unwrap() = vec![generate_random_string()].into(); - -// let subject = test_subject(); -// OfferTestFramework::with(test_issuance_services()) -// .given(vec![ -// OfferEvent::CredentialOfferCreated { -// offer_id: Default::default(), -// pre_authorized_code: subject.pre_authorized_code.clone(), -// access_token: subject.access_token.clone(), -// }, -// OfferEvent::CredentialsAdded { -// offer_id: Default::default(), -// credential_ids: vec!["credential-id".to_string()], -// }, -// OfferEvent::FormUrlEncodedCredentialOfferCreated { -// offer_id: Default::default(), -// form_url_encoded_credential_offer: subject.form_url_encoded_credential_offer.clone(), -// }, -// OfferEvent::TokenResponseCreated { -// offer_id: Default::default(), -// token_response: token_response(subject.clone()), -// }, -// ]) -// .when(OfferCommand::VerifyCredentialRequest { -// offer_id: Default::default(), -// credential_issuer_metadata: CREDENTIAL_ISSUER_METADATA.clone(), -// authorization_server_metadata: AUTHORIZATION_SERVER_METADATA.clone(), -// credential_request: credential_request(subject.clone()).await, -// }) -// .then_expect_events(vec![OfferEvent::CredentialRequestVerified { -// offer_id: Default::default(), -// subject_id: SUBJECT_KEY_DID.identifier("did:key", Algorithm::EdDSA).await.unwrap(), -// }]); -// } - -// #[rstest] -// #[serial_test::serial] -// async fn test_create_credential_response() { -// *PRE_AUTHORIZED_CODES.lock().unwrap() = vec![generate_random_string()].into(); -// *ACCESS_TOKENS.lock().unwrap() = vec![generate_random_string()].into(); -// *C_NONCES.lock().unwrap() = vec![generate_random_string()].into(); - -// let subject = test_subject(); -// OfferTestFramework::with(test_issuance_services()) -// .given(vec![ -// OfferEvent::CredentialOfferCreated { -// offer_id: Default::default(), -// pre_authorized_code: subject.pre_authorized_code.clone(), -// access_token: subject.access_token.clone(), -// }, -// OfferEvent::CredentialsAdded { -// offer_id: Default::default(), -// credential_ids: vec!["credential-id".to_string()], -// }, -// OfferEvent::FormUrlEncodedCredentialOfferCreated { -// offer_id: Default::default(), -// form_url_encoded_credential_offer: subject.form_url_encoded_credential_offer.clone(), -// }, -// OfferEvent::TokenResponseCreated { -// offer_id: Default::default(), -// token_response: token_response(subject.clone()), -// }, -// OfferEvent::CredentialRequestVerified { -// offer_id: Default::default(), -// subject_id: SUBJECT_KEY_DID.identifier("did:key", Algorithm::EdDSA).await.unwrap(), -// }, -// ]) -// .when(OfferCommand::CreateCredentialResponse { -// offer_id: Default::default(), -// signed_credentials: vec![json!(OPENBADGE_VERIFIABLE_CREDENTIAL_JWT)], -// }) -// .then_expect_events(vec![OfferEvent::CredentialResponseCreated { -// offer_id: Default::default(), -// credential_response: credential_response(subject), -// }]); -// } - -// #[derive(Clone)] -// struct TestSubject { -// subject: Arc, -// credential: String, -// access_token: String, -// pre_authorized_code: String, -// form_url_encoded_credential_offer: String, -// c_nonce: String, -// } - -// lazy_static! { -// pub static ref PRE_AUTHORIZED_CODES: Mutex> = Mutex::new(vec![].into()); -// pub static ref ACCESS_TOKENS: Mutex> = Mutex::new(vec![].into()); -// pub static ref C_NONCES: Mutex> = Mutex::new(vec![].into()); -// pub static ref SUBJECT_KEY_DID: Arc = test_issuance_services().issuer.clone(); -// } - -// fn test_subject() -> TestSubject { -// let pre_authorized_code = PRE_AUTHORIZED_CODES.lock().unwrap()[0].clone(); - -// TestSubject { -// subject: SUBJECT_KEY_DID.clone(), -// credential: OPENBADGE_VERIFIABLE_CREDENTIAL_JWT.to_string(), -// pre_authorized_code: pre_authorized_code.clone(), -// access_token: ACCESS_TOKENS.lock().unwrap()[0].clone(), -// form_url_encoded_credential_offer: format!("openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fexample.com%2F%22%2C%22credential_configuration_ids%22%3A%5B%220%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22{pre_authorized_code}%22%7D%7D%7D"), -// c_nonce: C_NONCES.lock().unwrap()[0].clone(), -// } -// } - -// fn token_request(subject: TestSubject) -> TokenRequest { -// TokenRequest::PreAuthorizedCode { -// pre_authorized_code: subject.pre_authorized_code, -// tx_code: None, -// } -// } - -// fn token_response(subject: TestSubject) -> TokenResponse { -// TokenResponse { -// access_token: subject.access_token.clone(), -// token_type: "bearer".to_string(), -// expires_in: None, -// refresh_token: None, -// scope: None, -// c_nonce: Some(subject.c_nonce.clone()), -// c_nonce_expires_in: None, -// } -// } - -// async fn credential_request(subject: TestSubject) -> CredentialRequest { -// CredentialRequest { -// credential_format: CredentialFormats::JwtVcJson(Parameters { -// parameters: ( -// CredentialDefinition { -// type_: vec!["VerifiableCredential".to_string(), "OpenBadgeCredential".to_string()], -// credential_subject: Default::default(), -// }, -// None, -// ) -// .into(), -// }), -// proof: Some( -// KeyProofType::builder() -// .proof_type(ProofType::Jwt) -// .algorithm(Algorithm::EdDSA) -// .signer(subject.subject.clone()) -// .iss(subject.subject.identifier("did:key", Algorithm::EdDSA).await.unwrap()) -// .aud(CREDENTIAL_ISSUER_METADATA.credential_issuer.clone()) -// .iat(1571324800) -// .nonce(subject.c_nonce.clone()) -// .subject_syntax_type("did:key") -// .build() -// .await -// .unwrap(), -// ), -// } -// } - -// fn credential_response(subject: TestSubject) -> CredentialResponse { -// CredentialResponse { -// credential: CredentialResponseType::Immediate { -// credential: json!(subject.credential.clone()), -// notification_id: None, -// }, -// c_nonce: None, -// c_nonce_expires_in: None, -// } -// } -// } +#[cfg(test)] +pub mod tests { + use super::test_utils::*; + use super::*; + use agent_api_rest::issuance; + use agent_api_rest::API_VERSION; + use agent_issuance::offer::aggregate::test_utils::token_response; + use agent_issuance::server_config::aggregate::test_utils::credential_configurations_supported; + use agent_issuance::{startup_commands::startup_commands, state::initialize}; + use agent_secret_manager::service::Service; + use agent_shared::generate_random_string; + use agent_store::in_memory; + use axum::{ + body::Body, + http::{self, Request}, + }; + use cqrs_es::test::TestFramework; + use oid4vci::credential_offer::CredentialOffer; + use rstest::{fixture, rstest}; + use serde_json::json; + use tokio::net::TcpListener; + use tower::Service as _; + + type OfferTestFramework = TestFramework; + + async fn bootstrap_issuer_server() -> CredentialOffer { + let listener = TcpListener::bind("0.0.0.0:0").await.unwrap(); + let issuer_url = format!("http://{}", listener.local_addr().unwrap()); + + let issuance_state = in_memory::issuance_state(Service::default(), Default::default()).await; + initialize(&issuance_state, startup_commands(issuer_url.parse().unwrap())).await; + + let offer_id = generate_random_string(); + + let mut app = issuance::router(issuance_state); + + let _ = app + .call( + Request::builder() + .method(http::Method::POST) + .uri(&format!("{issuer_url}{API_VERSION}/credentials")) + .header(http::header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref()) + .body(Body::from( + serde_json::to_vec(&json!({ + "offerId": offer_id, + "credential": { + "credentialSubject": { + "first_name": "Ferris", + "last_name": "Rustacean", + "degree": { + "type": "MasterDegree", + "name": "Master of Oceanography" + } + }}, + "credentialConfigurationId": "badge" + })) + .unwrap(), + )) + .unwrap(), + ) + .await; + + let response = app + .call( + Request::builder() + .method(http::Method::POST) + .uri(&format!("{issuer_url}{API_VERSION}/offers")) + .header(http::header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref()) + .body(Body::from( + serde_json::to_vec(&json!({ + "offerId": offer_id + })) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); + + let credential_offer: CredentialOffer = String::from_utf8(body.to_vec()).unwrap().parse().unwrap(); + + tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + + credential_offer + } + + #[fixture] + async fn credential_offer_parameters() -> Box { + let credential_offer = bootstrap_issuer_server().await; + + match credential_offer { + CredentialOffer::CredentialOffer(credential_offer) => credential_offer, + _ => unreachable!(), + } + } + + #[rstest] + #[serial_test::serial] + #[tokio::test] + async fn test_receive_credential_offer( + offer_id: String, + #[future(awt)] credential_offer_parameters: Box, + credential_configurations_supported: HashMap, + ) { + OfferTestFramework::with(Service::default()) + .given_no_previous_events() + .when_async(OfferCommand::ReceiveCredentialOffer { + offer_id: offer_id.clone(), + credential_offer: CredentialOffer::CredentialOffer(credential_offer_parameters.clone()), + }) + .await + .then_expect_events(vec![OfferEvent::CredentialOfferReceived { + offer_id, + credential_offer: credential_offer_parameters, + credential_configurations: credential_configurations_supported, + }]); + } + + #[rstest] + #[serial_test::serial] + #[tokio::test] + async fn test_accept_credential_offer( + offer_id: String, + #[future(awt)] credential_offer_parameters: Box, + #[future(awt)] token_response: TokenResponse, + credential_configurations_supported: HashMap, + ) { + OfferTestFramework::with(Service::default()) + .given(vec![OfferEvent::CredentialOfferReceived { + offer_id: offer_id.clone(), + credential_offer: credential_offer_parameters, + credential_configurations: credential_configurations_supported, + }]) + .when_async(OfferCommand::AcceptCredentialOffer { + offer_id: offer_id.clone(), + }) + .await + .then_expect_events(vec![ + OfferEvent::CredentialOfferAccepted { + offer_id: offer_id.clone(), + status: Status::Accepted, + }, + OfferEvent::TokenResponseReceived { + offer_id, + token_response, + }, + ]); + } + + #[rstest] + #[serial_test::serial] + #[tokio::test] + async fn test_send_credential_request( + offer_id: String, + #[future(awt)] credential_offer_parameters: Box, + #[future(awt)] token_response: TokenResponse, + credential_configurations_supported: HashMap, + ) { + OfferTestFramework::with(Service::default()) + .given(vec![ + OfferEvent::CredentialOfferReceived { + offer_id: offer_id.clone(), + credential_offer: credential_offer_parameters, + credential_configurations: credential_configurations_supported, + }, + OfferEvent::CredentialOfferAccepted { + offer_id: offer_id.clone(), + status: Status::Accepted, + }, + OfferEvent::TokenResponseReceived { + offer_id: offer_id.clone(), + token_response + }, + ]) + .when_async(OfferCommand::SendCredentialRequest { + offer_id: offer_id.clone(), + }) + .await + .then_expect_events(vec![OfferEvent::CredentialResponseReceived { + offer_id: offer_id.clone(), + status: Status::Received, + credentials: vec![json!("eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJkZWdyZWUiOnsidHlwZSI6Ik1hc3RlckRlZ3JlZSIsIm5hbWUiOiJNYXN0ZXIgb2YgT2NlYW5vZ3JhcGh5In0sImZpcnN0X25hbWUiOiJGZXJyaXMiLCJsYXN0X25hbWUiOiJSdXN0YWNlYW4ifSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiJ9fQ.jQEpI7DhjOcmyhPEpfGARwcRyzor_fUvynb43-eqD9175FBoshENX0S-8qlloQ7vbT5gat8TjvcDlGDN720ZBw")], + }]); + } + + #[rstest] + #[serial_test::serial] + #[tokio::test] + async fn test_reject_credential_offer( + offer_id: String, + #[future(awt)] credential_offer_parameters: Box, + credential_configurations_supported: HashMap, + ) { + OfferTestFramework::with(Service::default()) + .given(vec![OfferEvent::CredentialOfferReceived { + offer_id: offer_id.clone(), + credential_offer: credential_offer_parameters, + credential_configurations: credential_configurations_supported, + }]) + .when_async(OfferCommand::RejectCredentialOffer { + offer_id: offer_id.clone(), + }) + .await + .then_expect_events(vec![OfferEvent::CredentialOfferRejected { + offer_id: offer_id.clone(), + status: Status::Rejected, + }]); + } +} + +#[cfg(feature = "test_utils")] +pub mod test_utils { + use agent_shared::generate_random_string; + use rstest::*; + + #[fixture] + pub fn offer_id() -> String { + generate_random_string() + } +} diff --git a/agent_holder/src/offer/command.rs b/agent_holder/src/offer/command.rs index 2c5c3359..b62b6d36 100644 --- a/agent_holder/src/offer/command.rs +++ b/agent_holder/src/offer/command.rs @@ -11,9 +11,6 @@ pub enum OfferCommand { AcceptCredentialOffer { offer_id: String, }, - SendTokenRequest { - offer_id: String, - }, SendCredentialRequest { offer_id: String, }, diff --git a/agent_holder/src/offer/event.rs b/agent_holder/src/offer/event.rs index 8df88a82..da3d6281 100644 --- a/agent_holder/src/offer/event.rs +++ b/agent_holder/src/offer/event.rs @@ -13,7 +13,7 @@ use super::aggregate::Status; pub enum OfferEvent { CredentialOfferReceived { offer_id: String, - credential_offer: CredentialOfferParameters, + credential_offer: Box, credential_configurations: HashMap, }, CredentialOfferAccepted { diff --git a/agent_holder/src/offer/queries/all_offers.rs b/agent_holder/src/offer/queries/all_offers.rs index f19df40f..a12a4ec4 100644 --- a/agent_holder/src/offer/queries/all_offers.rs +++ b/agent_holder/src/offer/queries/all_offers.rs @@ -87,7 +87,7 @@ impl View for AllOffersView { // Get the entry for the aggregate_id .entry(event.aggregate_id.clone()) // or insert a new one if it doesn't exist - .or_insert_with(Default::default) + .or_default() // update the view with the event .update(event); } diff --git a/agent_holder/src/offer/queries/mod.rs b/agent_holder/src/offer/queries/mod.rs index 98f5bc17..3a36bfb2 100644 --- a/agent_holder/src/offer/queries/mod.rs +++ b/agent_holder/src/offer/queries/mod.rs @@ -9,8 +9,7 @@ use cqrs_es::{ }; use oid4vci::{ credential_issuer::credential_configurations_supported::CredentialConfigurationsSupportedObject, - credential_offer::CredentialOfferParameters, credential_response::CredentialResponse, - token_response::TokenResponse, + credential_offer::CredentialOfferParameters, token_response::TokenResponse, }; use serde::{Deserialize, Serialize}; @@ -50,7 +49,7 @@ impl View for OfferView { credential_configurations, .. } => { - self.credential_offer.replace(credential_offer.clone()); + self.credential_offer.replace(*credential_offer.clone()); self.credential_configurations .replace(credential_configurations.clone()); } diff --git a/agent_holder/src/services.rs b/agent_holder/src/services.rs index 70e17497..2958b6dd 100644 --- a/agent_holder/src/services.rs +++ b/agent_holder/src/services.rs @@ -1,3 +1,4 @@ +use agent_secret_manager::service::Service; use agent_shared::config::{config, get_all_enabled_did_methods, get_preferred_did_method}; use jsonwebtoken::Algorithm; use oid4vc_core::{Subject, SubjectSyntaxType}; @@ -10,8 +11,8 @@ pub struct HolderServices { pub wallet: Wallet, } -impl HolderServices { - pub fn new(holder: Arc) -> Self { +impl Service for HolderServices { + fn new(holder: Arc) -> Self { let signing_algorithms_supported: Vec = config() .signing_algorithms_supported .iter() @@ -46,19 +47,3 @@ impl HolderServices { Self { holder, wallet } } } - -#[cfg(feature = "test_utils")] -pub mod test_utils { - use agent_secret_manager::secret_manager; - use agent_secret_manager::subject::Subject; - - use super::*; - - pub fn test_holder_services() -> Arc { - Arc::new(HolderServices::new(Arc::new(futures::executor::block_on(async { - Subject { - secret_manager: secret_manager().await, - } - })))) - } -} diff --git a/agent_issuance/Cargo.toml b/agent_issuance/Cargo.toml index 76f603e4..def94f36 100644 --- a/agent_issuance/Cargo.toml +++ b/agent_issuance/Cargo.toml @@ -25,21 +25,29 @@ reqwest.workspace = true serde.workspace = true serde_json.workspace = true thiserror.workspace = true +once_cell = { workspace = true, optional = true } tracing.workspace = true url.workspace = true uuid.workspace = true +# `test_utils` dependencies +lazy_static = { workspace = true, optional = true } +rstest = { workspace = true, optional = true } + [dev-dependencies] +agent_holder = { path = "../agent_holder", features = ["test_utils"] } agent_issuance = { path = ".", features = ["test_utils"] } +agent_secret_manager = { path = "../agent_secret_manager", features = ["test_utils"] } agent_shared = { path = "../agent_shared", features = ["test_utils"] } did_manager.workspace = true -lazy_static.workspace = true serial_test = "3.0" -tokio.workspace = true tracing-test.workspace = true async-std = { version = "1.5", features = ["attributes", "tokio1"] } -rstest.workspace = true [features] -test_utils = [] +test_utils = [ + "dep:lazy_static", + "dep:rstest", + "dep:once_cell" +] diff --git a/agent_issuance/src/credential/aggregate.rs b/agent_issuance/src/credential/aggregate.rs index 9a9a5533..2e24cc07 100644 --- a/agent_issuance/src/credential/aggregate.rs +++ b/agent_issuance/src/credential/aggregate.rs @@ -257,14 +257,12 @@ impl Aggregate for Credential { #[cfg(test)] pub mod credential_tests { - use std::collections::HashMap; - + use super::test_utils::*; use super::*; + use agent_secret_manager::service::Service; use jsonwebtoken::Algorithm; - use lazy_static::lazy_static; - use oid4vci::proof::KeyProofMetadata; - use oid4vci::ProofType; + use rstest::rstest; use serde_json::json; @@ -272,8 +270,7 @@ pub mod credential_tests { use crate::credential::aggregate::Credential; use crate::credential::event::CredentialEvent; - use crate::offer::aggregate::tests::SUBJECT_KEY_DID; - use crate::services::test_utils::test_issuance_services; + use crate::offer::aggregate::test_utils::SUBJECT_KEY_DID; type CredentialTestFramework = TestFramework; @@ -294,7 +291,7 @@ pub mod credential_tests { #[case] credential_configuration: CredentialConfigurationsSupportedObject, #[case] unsigned_credential: serde_json::Value, ) { - CredentialTestFramework::with(test_issuance_services()) + CredentialTestFramework::with(Service::default()) .given_no_previous_events() .when(CredentialCommand::CreateUnsignedCredential { data: Data { @@ -327,7 +324,7 @@ pub mod credential_tests { #[case] credential_configuration: CredentialConfigurationsSupportedObject, #[case] verifiable_credential_jwt: String, ) { - CredentialTestFramework::with(test_issuance_services()) + CredentialTestFramework::with(Service::default()) .given(vec![CredentialEvent::UnsignedCredentialCreated { data: Data { raw: unsigned_credential, @@ -342,9 +339,29 @@ pub mod credential_tests { signed_credential: json!(verifiable_credential_jwt), }]) } +} + +#[cfg(feature = "test_utils")] +pub mod test_utils { + use super::*; + use jsonwebtoken::Algorithm; + use lazy_static::lazy_static; + use oid4vci::{ + credential_format_profiles::{ + w3c_verifiable_credentials::jwt_vc_json::CredentialDefinition, CredentialFormats, Parameters, + }, + proof::KeyProofMetadata, + ProofType, + }; + use serde_json::json; + use std::collections::HashMap; + + pub const OPENBADGE_VERIFIABLE_CREDENTIAL_JWT: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjIuanNvbiJdLCJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9jcmVkZW50aWFscy8zNTI3IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIk9wZW5CYWRnZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiIsIm5hbWUiOiJUZWFtd29yayBCYWRnZSIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJ0eXBlIjpbIkFjaGlldmVtZW50U3ViamVjdCJdLCJhY2hpZXZlbWVudCI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vYWNoaWV2ZW1lbnRzLzIxc3QtY2VudHVyeS1za2lsbHMvdGVhbXdvcmsiLCJ0eXBlIjoiQWNoaWV2ZW1lbnQiLCJjcml0ZXJpYSI6eyJuYXJyYXRpdmUiOiJUZWFtIG1lbWJlcnMgYXJlIG5vbWluYXRlZCBmb3IgdGhpcyBiYWRnZSBieSB0aGVpciBwZWVycyBhbmQgcmVjb2duaXplZCB1cG9uIHJldmlldyBieSBFeGFtcGxlIENvcnAgbWFuYWdlbWVudC4ifSwiZGVzY3JpcHRpb24iOiJUaGlzIGJhZGdlIHJlY29nbml6ZXMgdGhlIGRldmVsb3BtZW50IG9mIHRoZSBjYXBhY2l0eSB0byBjb2xsYWJvcmF0ZSB3aXRoaW4gYSBncm91cCBlbnZpcm9ubWVudC4iLCJuYW1lIjoiVGVhbXdvcmsifX19fQ.SkC7IvpBGB9e98eobnE9qcLjs-yoZup3cieBla3DRTlcRezXEDPv4YRoUgffho9LJ0rkmfFPsPwb-owXMWyPAA"; + + pub const W3C_VC_VERIFIABLE_CREDENTIAL_JWT: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJmaXJzdF9uYW1lIjoiRmVycmlzIiwibGFzdF9uYW1lIjoiUnVzdGFjZWFuIiwiZGVncmVlIjp7InR5cGUiOiJNYXN0ZXJEZWdyZWUiLCJuYW1lIjoiTWFzdGVyIG9mIE9jZWFub2dyYXBoeSJ9fSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiJ9fQ.MUDBbPJfXe0G9sjVTF3RuR6ukRM0d4N57iMGNFcIKMFPIEdig12v-YFB0qfnSghGcQo8hUw3jzxZXTSJATEgBg"; lazy_static! { - static ref OPENBADGE_CREDENTIAL_CONFIGURATION: CredentialConfigurationsSupportedObject = + pub static ref OPENBADGE_CREDENTIAL_CONFIGURATION: CredentialConfigurationsSupportedObject = CredentialConfigurationsSupportedObject { credential_format: CredentialFormats::JwtVcJson(Parameters { parameters: ( @@ -357,7 +374,6 @@ pub mod credential_tests { .into(), }), cryptographic_binding_methods_supported: vec![ - "did:key".to_string(), "did:key".to_string(), "did:iota:rms".to_string(), "did:jwk".to_string(), @@ -377,7 +393,7 @@ pub mod credential_tests { })], ..Default::default() }; - static ref W3C_VC_CREDENTIAL_CONFIGURATION: CredentialConfigurationsSupportedObject = + pub static ref W3C_VC_CREDENTIAL_CONFIGURATION: CredentialConfigurationsSupportedObject = CredentialConfigurationsSupportedObject { credential_format: CredentialFormats::JwtVcJson(Parameters { parameters: ( @@ -390,10 +406,9 @@ pub mod credential_tests { .into(), }), cryptographic_binding_methods_supported: vec![ - "did:key".to_string(), - "did:key".to_string(), "did:iota:rms".to_string(), "did:jwk".to_string(), + "did:key".to_string(), ], credential_signing_alg_values_supported: vec!["EdDSA".to_string()], proof_types_supported: HashMap::from_iter(vec![( @@ -403,14 +418,16 @@ pub mod credential_tests { }, )]), display: vec![json!({ - "name": "Master Degree", + "locale": "en", + "name": "Verifiable Credential", "logo": { - "url": "https://example.com/logo.png" + "uri": "https://impierce.com/images/logo-blue.png", + "alt_text": "UniCore Logo", } })], ..Default::default() }; - static ref OPENBADGE_CREDENTIAL_SUBJECT: serde_json::Value = json!( + pub static ref OPENBADGE_CREDENTIAL_SUBJECT: serde_json::Value = json!( { "credentialSubject": { "type": [ "AchievementSubject" ], @@ -426,7 +443,7 @@ pub mod credential_tests { } } ); - static ref W3C_VC_CREDENTIAL_SUBJECT: serde_json::Value = json!( + pub static ref W3C_VC_CREDENTIAL_SUBJECT: serde_json::Value = json!( { "credentialSubject": { "first_name": "Ferris", @@ -438,7 +455,7 @@ pub mod credential_tests { } } ); - static ref UNSIGNED_OPENBADGE_CREDENTIAL: serde_json::Value = json!({ + pub static ref UNSIGNED_OPENBADGE_CREDENTIAL: serde_json::Value = json!({ "@context": [ "https://www.w3.org/2018/credentials/v1", "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json" @@ -454,7 +471,7 @@ pub mod credential_tests { "name": "Teamwork Badge", "credentialSubject": OPENBADGE_CREDENTIAL_SUBJECT["credentialSubject"].clone(), }); - static ref UNSIGNED_W3C_VC_CREDENTIAL: serde_json::Value = json!({ + pub static ref UNSIGNED_W3C_VC_CREDENTIAL: serde_json::Value = json!({ "@context": "https://www.w3.org/2018/credentials/v1", "type": [ "VerifiableCredential" ], "credentialSubject": W3C_VC_CREDENTIAL_SUBJECT["credentialSubject"].clone(), @@ -465,8 +482,4 @@ pub mod credential_tests { "issuanceDate": "2010-01-01T00:00:00Z" }); } - - pub const OPENBADGE_VERIFIABLE_CREDENTIAL_JWT: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjIuanNvbiJdLCJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9jcmVkZW50aWFscy8zNTI3IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIk9wZW5CYWRnZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiIsIm5hbWUiOiJUZWFtd29yayBCYWRnZSIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJ0eXBlIjpbIkFjaGlldmVtZW50U3ViamVjdCJdLCJhY2hpZXZlbWVudCI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vYWNoaWV2ZW1lbnRzLzIxc3QtY2VudHVyeS1za2lsbHMvdGVhbXdvcmsiLCJ0eXBlIjoiQWNoaWV2ZW1lbnQiLCJjcml0ZXJpYSI6eyJuYXJyYXRpdmUiOiJUZWFtIG1lbWJlcnMgYXJlIG5vbWluYXRlZCBmb3IgdGhpcyBiYWRnZSBieSB0aGVpciBwZWVycyBhbmQgcmVjb2duaXplZCB1cG9uIHJldmlldyBieSBFeGFtcGxlIENvcnAgbWFuYWdlbWVudC4ifSwiZGVzY3JpcHRpb24iOiJUaGlzIGJhZGdlIHJlY29nbml6ZXMgdGhlIGRldmVsb3BtZW50IG9mIHRoZSBjYXBhY2l0eSB0byBjb2xsYWJvcmF0ZSB3aXRoaW4gYSBncm91cCBlbnZpcm9ubWVudC4iLCJuYW1lIjoiVGVhbXdvcmsifX19fQ.SkC7IvpBGB9e98eobnE9qcLjs-yoZup3cieBla3DRTlcRezXEDPv4YRoUgffho9LJ0rkmfFPsPwb-owXMWyPAA"; - - pub const W3C_VC_VERIFIABLE_CREDENTIAL_JWT: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJmaXJzdF9uYW1lIjoiRmVycmlzIiwibGFzdF9uYW1lIjoiUnVzdGFjZWFuIiwiZGVncmVlIjp7InR5cGUiOiJNYXN0ZXJEZWdyZWUiLCJuYW1lIjoiTWFzdGVyIG9mIE9jZWFub2dyYXBoeSJ9fSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiJ9fQ.MUDBbPJfXe0G9sjVTF3RuR6ukRM0d4N57iMGNFcIKMFPIEdig12v-YFB0qfnSghGcQo8hUw3jzxZXTSJATEgBg"; } diff --git a/agent_issuance/src/offer/aggregate.rs b/agent_issuance/src/offer/aggregate.rs index 9f00a530..ad1bddd1 100644 --- a/agent_issuance/src/offer/aggregate.rs +++ b/agent_issuance/src/offer/aggregate.rs @@ -1,4 +1,3 @@ -use agent_shared::generate_random_string; use async_trait::async_trait; use cqrs_es::Aggregate; use oid4vc_core::Validator; @@ -50,14 +49,19 @@ impl Aggregate for Offer { offer_id, credential_issuer_metadata, } => { - #[cfg(test)] + #[cfg(feature = "test_utils")] let (pre_authorized_code, access_token) = { - let pre_authorized_code = tests::PRE_AUTHORIZED_CODES.lock().unwrap().pop_front().unwrap(); - let access_token = tests::ACCESS_TOKENS.lock().unwrap().pop_front().unwrap(); + let pre_authorized_code = test_utils::pre_authorized_code().await; + let access_token = test_utils::access_token().await; (pre_authorized_code, access_token) }; - #[cfg(not(test))] - let (pre_authorized_code, access_token) = { (generate_random_string(), generate_random_string()) }; + #[cfg(not(feature = "test_utils"))] + let (pre_authorized_code, access_token) = { + ( + agent_shared::generate_random_string(), + agent_shared::generate_random_string(), + ) + }; // TODO: This needs to be fixed when we implement Batch credentials. let credentials_supported = credential_issuer_metadata.credential_configurations_supported.clone(); @@ -111,10 +115,10 @@ impl Aggregate for Offer { offer_id, token_request, } => { - #[cfg(test)] - let c_nonce = tests::C_NONCES.lock().unwrap().pop_front().unwrap(); - #[cfg(not(test))] - let c_nonce = generate_random_string(); + #[cfg(feature = "test_utils")] + let c_nonce = test_utils::c_nonce().await; + #[cfg(not(feature = "test_utils"))] + let c_nonce = agent_shared::generate_random_string(); match token_request { TokenRequest::PreAuthorizedCode { .. } => Ok(vec![TokenResponseCreated { @@ -140,7 +144,7 @@ impl Aggregate for Offer { } => { let credential_issuer = CredentialIssuer { subject: services.issuer.clone(), - metadata: credential_issuer_metadata, + metadata: *credential_issuer_metadata, authorization_server_metadata: *authorization_server_metadata, }; @@ -231,66 +235,61 @@ impl Aggregate for Offer { #[cfg(test)] pub mod tests { - use super::*; - + use super::test_utils::*; + use crate::{ + credential::aggregate::test_utils::OPENBADGE_VERIFIABLE_CREDENTIAL_JWT, server_config::aggregate::test_utils::*, + }; + use agent_secret_manager::service::Service; use cqrs_es::test::TestFramework; use jsonwebtoken::Algorithm; - use lazy_static::lazy_static; + use oid4vc_core::Subject; use oid4vci::{ - credential_format_profiles::{ - w3c_verifiable_credentials::jwt_vc_json::CredentialDefinition, CredentialFormats, Parameters, + credential_issuer::{ + authorization_server_metadata::AuthorizationServerMetadata, + credential_issuer_metadata::CredentialIssuerMetadata, }, credential_request::CredentialRequest, - KeyProofType, ProofType, }; - use rstest::rstest; - use serde_json::json; - use std::{collections::VecDeque, sync::Mutex}; - use crate::{ - credential::aggregate::credential_tests::OPENBADGE_VERIFIABLE_CREDENTIAL_JWT, - server_config::aggregate::server_config_tests::{AUTHORIZATION_SERVER_METADATA, CREDENTIAL_ISSUER_METADATA}, - services::test_utils::test_issuance_services, - }; + use serde_json::json; type OfferTestFramework = TestFramework; - #[test] + #[rstest] #[serial_test::serial] - fn test_create_offer() { - *PRE_AUTHORIZED_CODES.lock().unwrap() = vec![generate_random_string()].into(); - *ACCESS_TOKENS.lock().unwrap() = vec![generate_random_string()].into(); - *C_NONCES.lock().unwrap() = vec![generate_random_string()].into(); - - let subject = test_subject(); - OfferTestFramework::with(test_issuance_services()) + async fn test_create_offer( + #[future(awt)] pre_authorized_code: String, + #[future(awt)] access_token: String, + credential_issuer_metadata: Box, + #[future(awt)] credential_offer: CredentialOffer, + ) { + OfferTestFramework::with(Service::default()) .given_no_previous_events() .when(OfferCommand::CreateCredentialOffer { offer_id: Default::default(), - credential_issuer_metadata: CREDENTIAL_ISSUER_METADATA.clone(), + credential_issuer_metadata, }) .then_expect_events(vec![OfferEvent::CredentialOfferCreated { offer_id: Default::default(), - credential_offer: subject.credential_offer.clone(), - pre_authorized_code: subject.pre_authorized_code, - access_token: subject.access_token, + credential_offer, + pre_authorized_code, + access_token, }]); } - #[test] + #[rstest] #[serial_test::serial] - fn test_add_credential() { - *PRE_AUTHORIZED_CODES.lock().unwrap() = vec![generate_random_string()].into(); - *ACCESS_TOKENS.lock().unwrap() = vec![generate_random_string()].into(); - *C_NONCES.lock().unwrap() = vec![generate_random_string()].into(); - - let subject = test_subject(); - OfferTestFramework::with(test_issuance_services()) + async fn test_add_credential( + #[future(awt)] pre_authorized_code: String, + #[future(awt)] access_token: String, + #[future(awt)] credential_offer: CredentialOffer, + ) { + OfferTestFramework::with(Service::default()) .given(vec![OfferEvent::CredentialOfferCreated { offer_id: Default::default(), - credential_offer: subject.credential_offer.clone(), - pre_authorized_code: subject.pre_authorized_code.clone(), - access_token: subject.access_token.clone(), + credential_offer, + pre_authorized_code, + access_token, }]) .when(OfferCommand::AddCredentials { offer_id: Default::default(), @@ -302,21 +301,21 @@ pub mod tests { }]); } - #[test] + #[rstest] #[serial_test::serial] - fn test_create_credential_offer() { - *PRE_AUTHORIZED_CODES.lock().unwrap() = vec![generate_random_string()].into(); - *ACCESS_TOKENS.lock().unwrap() = vec![generate_random_string()].into(); - *C_NONCES.lock().unwrap() = vec![generate_random_string()].into(); - - let subject = test_subject(); - OfferTestFramework::with(test_issuance_services()) + async fn test_create_credential_offer( + #[future(awt)] pre_authorized_code: String, + #[future(awt)] access_token: String, + #[future(awt)] credential_offer: CredentialOffer, + #[future(awt)] form_url_encoded_credential_offer: String, + ) { + OfferTestFramework::with(Service::default()) .given(vec![ OfferEvent::CredentialOfferCreated { offer_id: Default::default(), - credential_offer: subject.credential_offer.clone(), - pre_authorized_code: subject.pre_authorized_code, - access_token: subject.access_token, + credential_offer, + pre_authorized_code, + access_token, }, OfferEvent::CredentialsAdded { offer_id: Default::default(), @@ -328,25 +327,27 @@ pub mod tests { }) .then_expect_events(vec![OfferEvent::FormUrlEncodedCredentialOfferCreated { offer_id: Default::default(), - form_url_encoded_credential_offer: subject.form_url_encoded_credential_offer, + form_url_encoded_credential_offer, }]); } - #[test] + #[rstest] #[serial_test::serial] - fn test_create_token_response() { - *PRE_AUTHORIZED_CODES.lock().unwrap() = vec![generate_random_string()].into(); - *ACCESS_TOKENS.lock().unwrap() = vec![generate_random_string()].into(); - *C_NONCES.lock().unwrap() = vec![generate_random_string()].into(); - - let subject = test_subject(); - OfferTestFramework::with(test_issuance_services()) + async fn test_create_token_response( + #[future(awt)] pre_authorized_code: String, + #[future(awt)] access_token: String, + #[future(awt)] credential_offer: CredentialOffer, + #[future(awt)] form_url_encoded_credential_offer: String, + #[future(awt)] token_request: TokenRequest, + #[future(awt)] token_response: TokenResponse, + ) { + OfferTestFramework::with(Service::default()) .given(vec![ OfferEvent::CredentialOfferCreated { offer_id: Default::default(), - credential_offer: subject.credential_offer.clone(), - pre_authorized_code: subject.pre_authorized_code.clone(), - access_token: subject.access_token.clone(), + credential_offer, + pre_authorized_code, + access_token, }, OfferEvent::CredentialsAdded { offer_id: Default::default(), @@ -354,34 +355,40 @@ pub mod tests { }, OfferEvent::FormUrlEncodedCredentialOfferCreated { offer_id: Default::default(), - form_url_encoded_credential_offer: subject.form_url_encoded_credential_offer.clone(), + form_url_encoded_credential_offer, }, ]) .when(OfferCommand::CreateTokenResponse { offer_id: Default::default(), - token_request: token_request(subject.clone()), + token_request, }) .then_expect_events(vec![OfferEvent::TokenResponseCreated { offer_id: Default::default(), - token_response: token_response(subject), + token_response, }]); } + #[allow(clippy::too_many_arguments)] #[rstest] #[serial_test::serial] - async fn test_verify_credential_response() { - *PRE_AUTHORIZED_CODES.lock().unwrap() = vec![generate_random_string()].into(); - *ACCESS_TOKENS.lock().unwrap() = vec![generate_random_string()].into(); - *C_NONCES.lock().unwrap() = vec![generate_random_string()].into(); - - let subject = test_subject(); - OfferTestFramework::with(test_issuance_services()) + async fn test_verify_credential_response( + holder: &Arc, + #[future(awt)] pre_authorized_code: String, + #[future(awt)] access_token: String, + #[future(awt)] credential_offer: CredentialOffer, + #[future(awt)] form_url_encoded_credential_offer: String, + #[future(awt)] token_response: TokenResponse, + #[future(awt)] credential_request: CredentialRequest, + credential_issuer_metadata: Box, + authorization_server_metadata: Box, + ) { + OfferTestFramework::with(Service::default()) .given(vec![ OfferEvent::CredentialOfferCreated { offer_id: Default::default(), - credential_offer: subject.credential_offer.clone(), - pre_authorized_code: subject.pre_authorized_code.clone(), - access_token: subject.access_token.clone(), + credential_offer, + pre_authorized_code, + access_token, }, OfferEvent::CredentialsAdded { offer_id: Default::default(), @@ -389,40 +396,43 @@ pub mod tests { }, OfferEvent::FormUrlEncodedCredentialOfferCreated { offer_id: Default::default(), - form_url_encoded_credential_offer: subject.form_url_encoded_credential_offer.clone(), + form_url_encoded_credential_offer, }, OfferEvent::TokenResponseCreated { offer_id: Default::default(), - token_response: token_response(subject.clone()), + token_response, }, ]) .when(OfferCommand::VerifyCredentialRequest { offer_id: Default::default(), - credential_issuer_metadata: CREDENTIAL_ISSUER_METADATA.clone(), - authorization_server_metadata: AUTHORIZATION_SERVER_METADATA.clone(), - credential_request: credential_request(subject.clone()).await, + credential_issuer_metadata, + authorization_server_metadata, + credential_request, }) .then_expect_events(vec![OfferEvent::CredentialRequestVerified { offer_id: Default::default(), - subject_id: SUBJECT_KEY_DID.identifier("did:key", Algorithm::EdDSA).await.unwrap(), + subject_id: holder.identifier("did:key", Algorithm::EdDSA).await.unwrap(), }]); } #[rstest] #[serial_test::serial] - async fn test_create_credential_response() { - *PRE_AUTHORIZED_CODES.lock().unwrap() = vec![generate_random_string()].into(); - *ACCESS_TOKENS.lock().unwrap() = vec![generate_random_string()].into(); - *C_NONCES.lock().unwrap() = vec![generate_random_string()].into(); - - let subject = test_subject(); - OfferTestFramework::with(test_issuance_services()) + async fn test_create_credential_response( + holder: &Arc, + #[future(awt)] pre_authorized_code: String, + #[future(awt)] access_token: String, + #[future(awt)] credential_offer: CredentialOffer, + #[future(awt)] form_url_encoded_credential_offer: String, + #[future(awt)] token_response: TokenResponse, + credential_response: CredentialResponse, + ) { + OfferTestFramework::with(Service::default()) .given(vec![ OfferEvent::CredentialOfferCreated { offer_id: Default::default(), - credential_offer: subject.credential_offer.clone(), - pre_authorized_code: subject.pre_authorized_code.clone(), - access_token: subject.access_token.clone(), + credential_offer, + pre_authorized_code, + access_token, }, OfferEvent::CredentialsAdded { offer_id: Default::default(), @@ -430,15 +440,15 @@ pub mod tests { }, OfferEvent::FormUrlEncodedCredentialOfferCreated { offer_id: Default::default(), - form_url_encoded_credential_offer: subject.form_url_encoded_credential_offer.clone(), + form_url_encoded_credential_offer, }, OfferEvent::TokenResponseCreated { offer_id: Default::default(), - token_response: token_response(subject.clone()), + token_response, }, OfferEvent::CredentialRequestVerified { offer_id: Default::default(), - subject_id: SUBJECT_KEY_DID.identifier("did:key", Algorithm::EdDSA).await.unwrap(), + subject_id: holder.identifier("did:key", Algorithm::EdDSA).await.unwrap(), }, ]) .when(OfferCommand::CreateCredentialResponse { @@ -447,34 +457,91 @@ pub mod tests { }) .then_expect_events(vec![OfferEvent::CredentialResponseCreated { offer_id: Default::default(), - credential_response: credential_response(subject), + credential_response, }]); } +} - #[derive(Clone)] - struct TestSubject { - subject: Arc, - credential_offer: CredentialOffer, - credential: String, - access_token: String, - pre_authorized_code: String, - form_url_encoded_credential_offer: String, - c_nonce: String, - } +#[cfg(feature = "test_utils")] +pub mod test_utils { + pub use super::*; + use crate::{ + credential::aggregate::test_utils::OPENBADGE_VERIFIABLE_CREDENTIAL_JWT, server_config::aggregate::test_utils::*, + }; + use agent_secret_manager::service::Service; + use agent_shared::generate_random_string; + use jsonwebtoken::Algorithm; + use lazy_static::lazy_static; + use oid4vc_core::Subject; + use oid4vci::{ + credential_format_profiles::{ + w3c_verifiable_credentials::jwt_vc_json::CredentialDefinition, CredentialFormats, Parameters, + }, + credential_issuer::credential_issuer_metadata::CredentialIssuerMetadata, + credential_request::CredentialRequest, + KeyProofType, ProofType, + }; + use once_cell::sync::OnceCell; + pub use rstest::*; + use serde_json::json; + use url::Url; lazy_static! { - pub static ref PRE_AUTHORIZED_CODES: Mutex> = Mutex::new(vec![].into()); - pub static ref ACCESS_TOKENS: Mutex> = Mutex::new(vec![].into()); - pub static ref C_NONCES: Mutex> = Mutex::new(vec![].into()); - pub static ref SUBJECT_KEY_DID: Arc = test_issuance_services().issuer.clone(); + pub static ref SUBJECT_KEY_DID: Arc = IssuanceServices::default().issuer.clone(); + } + + static PRE_AUTHORIZED_CODE: OnceCell = OnceCell::new(); + static ACCESS_TOKEN: OnceCell = OnceCell::new(); + static C_NONCE: OnceCell = OnceCell::new(); + + #[fixture] + pub async fn pre_authorized_code() -> String { + PRE_AUTHORIZED_CODE.get_or_init(generate_random_string).clone() } - fn test_subject() -> TestSubject { - let pre_authorized_code = PRE_AUTHORIZED_CODES.lock().unwrap()[0].clone(); + #[fixture] + pub async fn access_token() -> String { + ACCESS_TOKEN.get_or_init(generate_random_string).clone() + } + + #[fixture] + pub async fn c_nonce() -> String { + C_NONCE.get_or_init(generate_random_string).clone() + } + + pub struct TestAttributes { + pub pre_authorized_code: String, + pub access_token: String, + pub c_nonce: String, + } - let credential_offer = CredentialOffer::CredentialOffer(Box::new(CredentialOfferParameters { - credential_issuer: CREDENTIAL_ISSUER_METADATA.credential_issuer.clone(), - credential_configuration_ids: CREDENTIAL_ISSUER_METADATA + #[fixture] + pub async fn attributes( + #[future(awt)] pre_authorized_code: String, + #[future(awt)] access_token: String, + #[future(awt)] c_nonce: String, + ) -> TestAttributes { + TestAttributes { + pre_authorized_code, + access_token, + c_nonce, + } + } + + #[fixture] + #[once] + pub fn holder() -> Arc { + SUBJECT_KEY_DID.clone() + } + + #[fixture] + pub async fn credential_offer( + #[future(awt)] pre_authorized_code: String, + credential_issuer_metadata: Box, + ) -> CredentialOffer { + CredentialOffer::CredentialOffer(Box::new(CredentialOfferParameters { + credential_issuer: credential_issuer_metadata.credential_issuer.clone(), + credential_configuration_ids: credential_issuer_metadata .credential_configurations_supported .keys() .cloned() @@ -482,43 +549,45 @@ pub mod tests { grants: Some(Grants { authorization_code: None, pre_authorized_code: Some(PreAuthorizedCode { - pre_authorized_code: pre_authorized_code.clone(), + pre_authorized_code, ..Default::default() }), }), - })); - - TestSubject { - subject: SUBJECT_KEY_DID.clone(), - credential: OPENBADGE_VERIFIABLE_CREDENTIAL_JWT.to_string(), - credential_offer, - pre_authorized_code: pre_authorized_code.clone(), - access_token: ACCESS_TOKENS.lock().unwrap()[0].clone(), - form_url_encoded_credential_offer: format!("openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fexample.com%2F%22%2C%22credential_configuration_ids%22%3A%5B%220%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22{pre_authorized_code}%22%7D%7D%7D"), - c_nonce: C_NONCES.lock().unwrap()[0].clone(), - } + })) + } + + #[fixture] + pub async fn form_url_encoded_credential_offer(#[future(awt)] pre_authorized_code: String) -> String { + format!("openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fexample.com%2F%22%2C%22credential_configuration_ids%22%3A%5B%22badge%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22{pre_authorized_code}%22%7D%7D%7D") } - fn token_request(subject: TestSubject) -> TokenRequest { + #[fixture] + pub async fn token_request(#[future(awt)] pre_authorized_code: String) -> TokenRequest { TokenRequest::PreAuthorizedCode { - pre_authorized_code: subject.pre_authorized_code, + pre_authorized_code, tx_code: None, } } - fn token_response(subject: TestSubject) -> TokenResponse { + #[fixture] + pub async fn token_response(#[future(awt)] access_token: String, #[future(awt)] c_nonce: String) -> TokenResponse { TokenResponse { - access_token: subject.access_token.clone(), + access_token, token_type: "bearer".to_string(), expires_in: None, refresh_token: None, scope: None, - c_nonce: Some(subject.c_nonce.clone()), + c_nonce: Some(c_nonce), c_nonce_expires_in: None, } } - async fn credential_request(subject: TestSubject) -> CredentialRequest { + #[fixture] + pub async fn credential_request( + #[future(awt)] c_nonce: String, + holder: &Arc, + static_issuer_url: &Url, + ) -> CredentialRequest { CredentialRequest { credential_format: CredentialFormats::JwtVcJson(Parameters { parameters: ( @@ -534,11 +603,11 @@ pub mod tests { KeyProofType::builder() .proof_type(ProofType::Jwt) .algorithm(Algorithm::EdDSA) - .signer(subject.subject.clone()) - .iss(subject.subject.identifier("did:key", Algorithm::EdDSA).await.unwrap()) - .aud(CREDENTIAL_ISSUER_METADATA.credential_issuer.clone()) + .signer(holder.clone()) + .iss(holder.identifier("did:key", Algorithm::EdDSA).await.unwrap()) + .aud(static_issuer_url.to_string()) .iat(1571324800) - .nonce(subject.c_nonce.clone()) + .nonce(c_nonce) .subject_syntax_type("did:key") .build() .await @@ -547,10 +616,11 @@ pub mod tests { } } - fn credential_response(subject: TestSubject) -> CredentialResponse { + #[fixture] + pub fn credential_response() -> CredentialResponse { CredentialResponse { credential: CredentialResponseType::Immediate { - credential: json!(subject.credential.clone()), + credential: json!(OPENBADGE_VERIFIABLE_CREDENTIAL_JWT.to_string()), notification_id: None, }, c_nonce: None, diff --git a/agent_issuance/src/offer/command.rs b/agent_issuance/src/offer/command.rs index ad58d100..1dbb22fe 100644 --- a/agent_issuance/src/offer/command.rs +++ b/agent_issuance/src/offer/command.rs @@ -14,7 +14,7 @@ use url::Url; pub enum OfferCommand { CreateCredentialOffer { offer_id: String, - credential_issuer_metadata: CredentialIssuerMetadata, + credential_issuer_metadata: Box, }, AddCredentials { offer_id: String, @@ -36,7 +36,7 @@ pub enum OfferCommand { }, VerifyCredentialRequest { offer_id: String, - credential_issuer_metadata: CredentialIssuerMetadata, + credential_issuer_metadata: Box, authorization_server_metadata: Box, credential_request: CredentialRequest, }, diff --git a/agent_issuance/src/server_config/aggregate.rs b/agent_issuance/src/server_config/aggregate.rs index f4284d57..911dc1ae 100644 --- a/agent_issuance/src/server_config/aggregate.rs +++ b/agent_issuance/src/server_config/aggregate.rs @@ -124,7 +124,7 @@ impl Aggregate for ServerConfig { credential_issuer_metadata, } => { self.authorization_server_metadata = *authorization_server_metadata; - self.credential_issuer_metadata = credential_issuer_metadata; + self.credential_issuer_metadata = *credential_issuer_metadata; } CredentialConfigurationAdded { credential_configurations, @@ -135,110 +135,112 @@ impl Aggregate for ServerConfig { #[cfg(test)] pub mod server_config_tests { - use std::collections::HashMap; - + use super::test_utils::*; use super::*; - + use crate::server_config::aggregate::ServerConfig; + use crate::server_config::event::ServerConfigEvent; use agent_shared::config::CredentialConfiguration; - use lazy_static::lazy_static; + use cqrs_es::test::TestFramework; use oid4vci::credential_format_profiles::w3c_verifiable_credentials::jwt_vc_json::JwtVcJson; use oid4vci::credential_format_profiles::{w3c_verifiable_credentials, CredentialFormats, Parameters}; - use oid4vci::credential_issuer::credential_configurations_supported::CredentialConfigurationsSupportedObject; + use rstest::*; use serde_json::json; - use cqrs_es::test::TestFramework; - - use crate::server_config::aggregate::ServerConfig; - use crate::server_config::event::ServerConfigEvent; - type ServerConfigTestFramework = TestFramework; - #[test] - fn test_load_server_metadata() { + #[rstest] + fn test_load_server_metadata( + authorization_server_metadata: Box, + credential_issuer_metadata: Box, + ) { ServerConfigTestFramework::with(()) .given_no_previous_events() .when(ServerConfigCommand::InitializeServerMetadata { - authorization_server_metadata: AUTHORIZATION_SERVER_METADATA.clone(), - credential_issuer_metadata: CREDENTIAL_ISSUER_METADATA.clone(), + authorization_server_metadata: authorization_server_metadata.clone(), + credential_issuer_metadata: credential_issuer_metadata.clone(), }) .then_expect_events(vec![ServerConfigEvent::ServerMetadataInitialized { - authorization_server_metadata: AUTHORIZATION_SERVER_METADATA.clone(), - credential_issuer_metadata: CREDENTIAL_ISSUER_METADATA.clone(), + authorization_server_metadata, + credential_issuer_metadata, }]); } - #[test] - fn test_create_credentials_supported() { + #[rstest] + fn test_create_credentials_supported( + authorization_server_metadata: Box, + credential_issuer_metadata: Box, + ) { ServerConfigTestFramework::with(()) .given(vec![ServerConfigEvent::ServerMetadataInitialized { - authorization_server_metadata: AUTHORIZATION_SERVER_METADATA.clone(), - credential_issuer_metadata: CREDENTIAL_ISSUER_METADATA.clone(), + authorization_server_metadata, + credential_issuer_metadata: credential_issuer_metadata.clone(), }]) .when(ServerConfigCommand::AddCredentialConfiguration { credential_configuration: CredentialConfiguration { - credential_configuration_id: "0".to_string(), + credential_configuration_id: "badge".to_string(), credential_format_with_parameters: CredentialFormats::JwtVcJson(Parameters:: { parameters: w3c_verifiable_credentials::jwt_vc_json::JwtVcJsonParameters { credential_definition: w3c_verifiable_credentials::jwt_vc_json::CredentialDefinition { - type_: vec!["VerifiableCredential".to_string(), "OpenBadgeCredential".to_string()], + type_: vec!["VerifiableCredential".to_string()], credential_subject: Default::default(), }, order: None, }, }), - display: vec![], + display: vec![json!({ + "name": "Verifiable Credential", + "locale": "en", + "logo": { + "uri": "https://impierce.com/images/logo-blue.png", + "alt_text": "UniCore Logo" + } + })], }, }) .then_expect_events(vec![ServerConfigEvent::CredentialConfigurationAdded { - credential_configurations: CREDENTIAL_CONFIGURATIONS_SUPPORTED.clone(), + credential_configurations: credential_issuer_metadata.credential_configurations_supported, }]); } +} - lazy_static! { - static ref BASE_URL: url::Url = "https://example.com/".parse().unwrap(); - static ref CREDENTIAL_CONFIGURATIONS_SUPPORTED: HashMap = - vec![( - "0".to_string(), - serde_json::from_value(json!({ - "format": "jwt_vc_json", - "cryptographic_binding_methods_supported": [ - "did:iota:rms", - "did:jwk", - "did:key", - ], - "credential_signing_alg_values_supported": [ - "EdDSA" - ], - "proof_types_supported": { - "jwt": { - "proof_signing_alg_values_supported": [ - "EdDSA" - ] - } - }, - "credential_definition":{ - "type": [ - "VerifiableCredential", - "OpenBadgeCredential" - ] - } - } - )) - .unwrap() - )] - .into_iter() - .collect(); - pub static ref AUTHORIZATION_SERVER_METADATA: Box = - Box::new(AuthorizationServerMetadata { - issuer: BASE_URL.clone(), - token_endpoint: Some(BASE_URL.join("token").unwrap()), - ..Default::default() - }); - pub static ref CREDENTIAL_ISSUER_METADATA: CredentialIssuerMetadata = CredentialIssuerMetadata { - credential_issuer: BASE_URL.clone(), - credential_endpoint: BASE_URL.join("credential").unwrap(), - batch_credential_endpoint: Some(BASE_URL.join("batch_credential").unwrap()), - credential_configurations_supported: CREDENTIAL_CONFIGURATIONS_SUPPORTED.clone(), +#[cfg(feature = "test_utils")] +pub mod test_utils { + use super::*; + use crate::credential::aggregate::test_utils::W3C_VC_CREDENTIAL_CONFIGURATION; + use oid4vci::credential_issuer::credential_issuer_metadata::CredentialIssuerMetadata; + use rstest::*; + use url::Url; + + #[fixture] + #[once] + pub fn static_issuer_url() -> url::Url { + "https://example.com/".parse().unwrap() + } + + #[fixture] + pub fn credential_configurations_supported() -> HashMap { + HashMap::from_iter(vec![("badge".to_string(), W3C_VC_CREDENTIAL_CONFIGURATION.clone())]) + } + + #[fixture] + pub fn authorization_server_metadata(static_issuer_url: &Url) -> Box { + Box::new(AuthorizationServerMetadata { + issuer: static_issuer_url.clone(), + token_endpoint: Some(static_issuer_url.join("token").unwrap()), + ..Default::default() + }) + } + + #[fixture] + pub fn credential_issuer_metadata( + static_issuer_url: &Url, + credential_configurations_supported: HashMap, + ) -> Box { + Box::new(CredentialIssuerMetadata { + credential_issuer: static_issuer_url.clone(), + credential_endpoint: static_issuer_url.join("credential").unwrap(), + batch_credential_endpoint: Some(static_issuer_url.join("batch_credential").unwrap()), + credential_configurations_supported, ..Default::default() - }; + }) } } diff --git a/agent_issuance/src/server_config/command.rs b/agent_issuance/src/server_config/command.rs index d0367e4a..6f5a5c7f 100644 --- a/agent_issuance/src/server_config/command.rs +++ b/agent_issuance/src/server_config/command.rs @@ -9,7 +9,7 @@ use serde::Deserialize; pub enum ServerConfigCommand { InitializeServerMetadata { authorization_server_metadata: Box, - credential_issuer_metadata: CredentialIssuerMetadata, + credential_issuer_metadata: Box, }, AddCredentialConfiguration { credential_configuration: CredentialConfiguration, diff --git a/agent_issuance/src/server_config/event.rs b/agent_issuance/src/server_config/event.rs index 60583df6..56e6078d 100644 --- a/agent_issuance/src/server_config/event.rs +++ b/agent_issuance/src/server_config/event.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; pub enum ServerConfigEvent { ServerMetadataInitialized { authorization_server_metadata: Box, - credential_issuer_metadata: CredentialIssuerMetadata, + credential_issuer_metadata: Box, }, CredentialConfigurationAdded { credential_configurations: HashMap, diff --git a/agent_issuance/src/server_config/queries.rs b/agent_issuance/src/server_config/queries.rs index 80cf8e1e..72f7177b 100644 --- a/agent_issuance/src/server_config/queries.rs +++ b/agent_issuance/src/server_config/queries.rs @@ -23,7 +23,7 @@ impl View for ServerConfigView { } => { self.authorization_server_metadata = *authorization_server_metadata.clone(); self.credential_issuer_metadata - .replace(credential_issuer_metadata.clone()); + .replace(*credential_issuer_metadata.clone()); } CredentialConfigurationAdded { credential_configurations, diff --git a/agent_issuance/src/services.rs b/agent_issuance/src/services.rs index 49830325..2e4b4353 100644 --- a/agent_issuance/src/services.rs +++ b/agent_issuance/src/services.rs @@ -1,3 +1,4 @@ +use agent_secret_manager::service::Service; use oid4vc_core::Subject; use std::sync::Arc; @@ -6,24 +7,8 @@ pub struct IssuanceServices { pub issuer: Arc, } -impl IssuanceServices { - pub fn new(issuer: Arc) -> Self { +impl Service for IssuanceServices { + fn new(issuer: Arc) -> Self { Self { issuer } } } - -#[cfg(feature = "test_utils")] -pub mod test_utils { - use agent_secret_manager::secret_manager; - use agent_secret_manager::subject::Subject; - - use super::*; - - pub fn test_issuance_services() -> Arc { - Arc::new(IssuanceServices::new(Arc::new(futures::executor::block_on(async { - Subject { - secret_manager: secret_manager().await, - } - })))) - } -} diff --git a/agent_issuance/src/startup_commands.rs b/agent_issuance/src/startup_commands.rs index 2423cd50..6f1ae9a1 100644 --- a/agent_issuance/src/startup_commands.rs +++ b/agent_issuance/src/startup_commands.rs @@ -23,12 +23,12 @@ pub fn load_server_metadata(base_url: url::Url) -> ServerConfigCommand { token_endpoint: Some(base_url.append_path_segment("auth/token")), ..Default::default() }), - credential_issuer_metadata: CredentialIssuerMetadata { + credential_issuer_metadata: Box::new(CredentialIssuerMetadata { credential_issuer: base_url.clone(), credential_endpoint: base_url.append_path_segment("openid4vci/credential"), display, ..Default::default() - }, + }), } } diff --git a/agent_secret_manager/Cargo.toml b/agent_secret_manager/Cargo.toml index e257bc3e..4c3f37be 100644 --- a/agent_secret_manager/Cargo.toml +++ b/agent_secret_manager/Cargo.toml @@ -12,6 +12,7 @@ async-trait = "0.1" base64.workspace = true cqrs-es = "0.4.2" did_manager.workspace = true +futures.workspace = true identity_iota.workspace = true jsonwebtoken = "9.3" log = "0.4" @@ -28,3 +29,6 @@ agent_shared = { path = "../agent_shared", features = ["test_utils"] } futures.workspace = true lazy_static.workspace = true ring = "0.17.8" + +[features] +test_utils = [] diff --git a/agent_secret_manager/src/lib.rs b/agent_secret_manager/src/lib.rs index e0b2b946..60e4ccba 100644 --- a/agent_secret_manager/src/lib.rs +++ b/agent_secret_manager/src/lib.rs @@ -2,6 +2,7 @@ use agent_shared::config::{config, SecretManagerConfig}; use did_manager::SecretManager; use log::info; +pub mod service; pub mod subject; // TODO: find better solution for this diff --git a/agent_secret_manager/src/service.rs b/agent_secret_manager/src/service.rs new file mode 100644 index 00000000..9c9a1748 --- /dev/null +++ b/agent_secret_manager/src/service.rs @@ -0,0 +1,19 @@ +use std::sync::Arc; + +pub trait Service { + fn new(subject: Arc) -> Self; + + #[cfg(feature = "test_utils")] + fn default() -> Arc + where + Self: Sized, + { + use crate::{secret_manager, subject::Subject}; + + Arc::new(Self::new(Arc::new(futures::executor::block_on(async { + Subject { + secret_manager: secret_manager().await, + } + })))) + } +} diff --git a/agent_shared/Cargo.toml b/agent_shared/Cargo.toml index a3084be3..519d2fd1 100644 --- a/agent_shared/Cargo.toml +++ b/agent_shared/Cargo.toml @@ -23,7 +23,7 @@ identity_iota.workspace = true oid4vc-core.workspace = true oid4vci.workspace = true oid4vp.workspace = true -once_cell = { version = "1.19" } +once_cell.workspace = true rand = "0.8" serde.workspace = true serde_json.workspace = true diff --git a/agent_verification/src/authorization_request/aggregate.rs b/agent_verification/src/authorization_request/aggregate.rs index cd7692a4..de46256d 100644 --- a/agent_verification/src/authorization_request/aggregate.rs +++ b/agent_verification/src/authorization_request/aggregate.rs @@ -160,6 +160,7 @@ pub mod tests { use std::str::FromStr; use agent_secret_manager::secret_manager; + use agent_secret_manager::service::Service as _; use agent_secret_manager::subject::Subject; use agent_shared::config::set_config; use agent_shared::config::SupportedDidMethod; @@ -172,8 +173,6 @@ pub mod tests { use rstest::rstest; use serde_json::json; - use crate::services::test_utils::test_verification_services; - use super::*; type AuthorizationRequestTestFramework = TestFramework; @@ -186,7 +185,7 @@ pub mod tests { ) { set_config().set_preferred_did_method(verifier_did_method.clone()); - let verification_services = test_verification_services(); + let verification_services = VerificationServices::default(); let siopv2_client_metadata = verification_services.siopv2_client_metadata.clone(); let oid4vp_client_metadata = verification_services.oid4vp_client_metadata.clone(); @@ -225,7 +224,7 @@ pub mod tests { ) { set_config().set_preferred_did_method(verifier_did_method.clone()); - let verification_services = test_verification_services(); + let verification_services = VerificationServices::default(); let siopv2_client_metadata = verification_services.siopv2_client_metadata.clone(); let oid4vp_client_metadata = verification_services.oid4vp_client_metadata.clone(); diff --git a/agent_verification/src/connection/aggregate.rs b/agent_verification/src/connection/aggregate.rs index 312afb8c..587271d5 100644 --- a/agent_verification/src/connection/aggregate.rs +++ b/agent_verification/src/connection/aggregate.rs @@ -117,7 +117,7 @@ pub mod tests { authorization_request, verifier_did, PRESENTATION_DEFINITION, }; use crate::generic_oid4vc::GenericAuthorizationRequest; - use crate::services::test_utils::test_verification_services; + use agent_secret_manager::service::Service as _; use super::*; @@ -136,7 +136,7 @@ pub mod tests { ) { set_config().set_preferred_did_method(verifier_did_method.clone()); - let verification_services = test_verification_services(); + let verification_services = VerificationServices::default(); let siopv2_client_metadata = verification_services.siopv2_client_metadata.clone(); let oid4vp_client_metadata = verification_services.oid4vp_client_metadata.clone(); diff --git a/agent_verification/src/services.rs b/agent_verification/src/services.rs index 92de8ac9..613b79ec 100644 --- a/agent_verification/src/services.rs +++ b/agent_verification/src/services.rs @@ -1,3 +1,4 @@ +use agent_secret_manager::service::Service; use agent_shared::config::{config, get_all_enabled_did_methods, get_preferred_did_method}; use jsonwebtoken::Algorithm; use oid4vc_core::{client_metadata::ClientMetadataResource, Subject}; @@ -14,8 +15,8 @@ pub struct VerificationServices { pub oid4vp_client_metadata: ClientMetadataResource, } -impl VerificationServices { - pub fn new(verifier: Arc) -> Self { +impl Service for VerificationServices { + fn new(verifier: Arc) -> Self { let client_name = config().display.first().as_ref().map(|display| display.name.clone()); let logo_uri = config() @@ -83,21 +84,3 @@ impl VerificationServices { } } } - -#[cfg(feature = "test_utils")] -pub mod test_utils { - use agent_secret_manager::secret_manager; - use agent_secret_manager::subject::Subject; - - use super::*; - - pub fn test_verification_services() -> Arc { - Arc::new(VerificationServices::new(Arc::new(futures::executor::block_on( - async { - Subject { - secret_manager: secret_manager().await, - } - }, - )))) - } -} From 805591e499de509639c0cd457f434de6d193884c Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 30 Aug 2024 02:33:01 +0200 Subject: [PATCH 16/31] refactor: deprecate `path` closure --- agent_api_rest/src/lib.rs | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index 2fa9e367..b282b706 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -19,26 +19,16 @@ pub struct ApplicationState { pub verification_state: Option, } -pub fn app(state: ApplicationState) -> Router { - let ApplicationState { +pub fn app( + ApplicationState { issuance_state, holder_state, verification_state, - } = state; - - let base_path = get_base_path(); - - let path = |suffix: &str| -> String { - if let Ok(base_path) = &base_path { - format!("/{}{}", base_path, suffix) - } else { - suffix.to_string() - } - }; - + }: ApplicationState, +) -> Router { Router::new() .nest( - &path(Default::default()), + &get_base_path().unwrap_or_default(), Router::new() .merge(issuance_state.map(issuance::router).unwrap_or_default()) .merge(holder_state.map(holder::router).unwrap_or_default()) @@ -89,7 +79,7 @@ fn get_base_path() -> Result { tracing::info!("Base path: {:?}", base_path); - base_path + format!("/{}", base_path) }) } From dc8c25d260e832538effcc67d174a2ca597843a3 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 30 Aug 2024 02:39:46 +0200 Subject: [PATCH 17/31] refactor: remove unused dependencies --- Cargo.lock | 10 ---------- agent_holder/Cargo.toml | 11 ----------- agent_holder/src/credential/aggregate.rs | 4 +--- 3 files changed, 1 insertion(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b25ceaf..1545b186 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,20 +160,13 @@ dependencies = [ "async-std", "async-trait", "axum 0.7.5", - "chrono", "cqrs-es", - "derivative", "did_manager", - "futures", - "identity_core", - "identity_credential", - "jsonschema", "jsonwebtoken", "lazy_static", "mime", "names", "oid4vc-core", - "oid4vc-manager", "oid4vci", "rand 0.8.5", "reqwest 0.12.5", @@ -186,9 +179,6 @@ dependencies = [ "tower", "tracing", "tracing-test", - "types-ob-v3", - "url", - "uuid", ] [[package]] diff --git a/agent_holder/Cargo.toml b/agent_holder/Cargo.toml index 0601c04f..974ada58 100644 --- a/agent_holder/Cargo.toml +++ b/agent_holder/Cargo.toml @@ -10,23 +10,13 @@ agent_secret_manager = { path = "../agent_secret_manager" } async-trait.workspace = true cqrs-es.workspace = true -chrono = "0.4" -types-ob-v3 = { git = "https://github.com/impierce/digital-credential-data-models.git", rev = "9f16c27" } -derivative = "2.2" -futures.workspace = true -identity_core = "1.3" -identity_credential.workspace = true -jsonschema = "0.17" jsonwebtoken.workspace = true oid4vci.workspace = true oid4vc-core.workspace = true -oid4vc-manager.workspace = true serde.workspace = true serde_json.workspace = true thiserror.workspace = true tracing.workspace = true -url.workspace = true -uuid.workspace = true # `test_utils` dependencies rstest = { workspace = true, optional = true } @@ -38,7 +28,6 @@ agent_issuance = { path = "../agent_issuance", features = ["test_utils"] } agent_secret_manager = { path = "../agent_secret_manager", features = ["test_utils"] } agent_shared = { path = "../agent_shared", features = ["test_utils"] } agent_store = { path = "../agent_store" } -# agent_verification = { path = "../agent_verification", features = ["test_utils"] } axum.workspace = true did_manager.workspace = true diff --git a/agent_holder/src/credential/aggregate.rs b/agent_holder/src/credential/aggregate.rs index 97df95a6..5488ccf1 100644 --- a/agent_holder/src/credential/aggregate.rs +++ b/agent_holder/src/credential/aggregate.rs @@ -4,13 +4,11 @@ use crate::credential::event::CredentialEvent; use crate::services::HolderServices; use async_trait::async_trait; use cqrs_es::Aggregate; -use derivative::Derivative; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tracing::info; -#[derive(Debug, Clone, Serialize, Deserialize, Default, Derivative)] -#[derivative(PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Credential { pub credential_id: Option, pub offer_id: Option, From 9f20fb6570590738f3bebde07f2cdcab1a4074b8 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 30 Aug 2024 10:00:49 +0200 Subject: [PATCH 18/31] style: add clippy exception --- agent_api_rest/src/holder/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/agent_api_rest/src/holder/mod.rs b/agent_api_rest/src/holder/mod.rs index fcd77932..93f06f86 100644 --- a/agent_api_rest/src/holder/mod.rs +++ b/agent_api_rest/src/holder/mod.rs @@ -1,3 +1,5 @@ +// TODO: further refactor the API's folder structure to reflect the API's routes. +#[allow(clippy::module_inception)] pub mod holder; pub mod openid4vci; From 7c139293b2e05e1f059345049e07a30ef14329a9 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 30 Aug 2024 10:06:37 +0200 Subject: [PATCH 19/31] build: bump oid4vc dependencies --- Cargo.lock | 12 ++++++------ Cargo.toml | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1545b186..4590b2d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2067,7 +2067,7 @@ dependencies = [ [[package]] name = "dif-presentation-exchange" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=6c1cdb6#6c1cdb66b93c091725ca8709ee33e03f28ad65a2" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=23facd4#23facd4946e4b2e54ef87f42746c68ad952ae205" dependencies = [ "getset", "jsonpath_lib", @@ -4724,7 +4724,7 @@ dependencies = [ [[package]] name = "oid4vc-core" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=6c1cdb6#6c1cdb66b93c091725ca8709ee33e03f28ad65a2" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=23facd4#23facd4946e4b2e54ef87f42746c68ad952ae205" dependencies = [ "anyhow", "async-trait", @@ -4748,7 +4748,7 @@ dependencies = [ [[package]] name = "oid4vc-manager" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=6c1cdb6#6c1cdb66b93c091725ca8709ee33e03f28ad65a2" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=23facd4#23facd4946e4b2e54ef87f42746c68ad952ae205" dependencies = [ "anyhow", "async-trait", @@ -4780,7 +4780,7 @@ dependencies = [ [[package]] name = "oid4vci" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=6c1cdb6#6c1cdb66b93c091725ca8709ee33e03f28ad65a2" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=23facd4#23facd4946e4b2e54ef87f42746c68ad952ae205" dependencies = [ "anyhow", "derivative", @@ -4803,7 +4803,7 @@ dependencies = [ [[package]] name = "oid4vp" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=6c1cdb6#6c1cdb66b93c091725ca8709ee33e03f28ad65a2" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=23facd4#23facd4946e4b2e54ef87f42746c68ad952ae205" dependencies = [ "anyhow", "chrono", @@ -6666,7 +6666,7 @@ dependencies = [ [[package]] name = "siopv2" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=6c1cdb6#6c1cdb66b93c091725ca8709ee33e03f28ad65a2" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=23facd4#23facd4946e4b2e54ef87f42746c68ad952ae205" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 17958668..8373b848 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,11 +19,11 @@ rust-version = "1.76.0" [workspace.dependencies] did_manager = { git = "https://git@github.com/impierce/did-manager.git", rev = "2bda2b8" } -siopv2 = { git = "https://git@github.com/impierce/openid4vc.git", rev = "6c1cdb6" } -oid4vci = { git = "https://git@github.com/impierce/openid4vc.git", rev = "6c1cdb6" } -oid4vc-core = { git = "https://git@github.com/impierce/openid4vc.git", rev = "6c1cdb6" } -oid4vc-manager = { git = "https://git@github.com/impierce/openid4vc.git", rev = "6c1cdb6" } -oid4vp = { git = "https://git@github.com/impierce/openid4vc.git", rev = "6c1cdb6" } +siopv2 = { git = "https://git@github.com/impierce/openid4vc.git", rev = "23facd4" } +oid4vci = { git = "https://git@github.com/impierce/openid4vc.git", rev = "23facd4" } +oid4vc-core = { git = "https://git@github.com/impierce/openid4vc.git", rev = "23facd4" } +oid4vc-manager = { git = "https://git@github.com/impierce/openid4vc.git", rev = "23facd4" } +oid4vp = { git = "https://git@github.com/impierce/openid4vc.git", rev = "23facd4" } async-trait = "0.1" axum = { version = "0.7", features = ["tracing"] } From 384244cc5c7969f7192b56c37476a3e0a42ab2ef Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 30 Aug 2024 14:28:41 +0200 Subject: [PATCH 20/31] refactor: move all `CustomQuery` logic to `agent_shared` --- .../src/credential/queries/all_credentials.rs | 81 ++--------------- agent_holder/src/credential/queries/mod.rs | 19 +--- agent_holder/src/offer/queries/all_offers.rs | 81 ++--------------- agent_holder/src/offer/queries/mod.rs | 28 +----- .../src/offer/queries/access_token.rs | 5 +- agent_issuance/src/offer/queries/mod.rs | 25 +----- .../src/offer/queries/pre_authorized_code.rs | 5 +- agent_shared/src/custom_queries.rs | 88 +++++++++++++++++++ agent_shared/src/lib.rs | 1 + agent_store/src/in_memory.rs | 14 ++- agent_store/src/postgres.rs | 16 ++-- 11 files changed, 125 insertions(+), 238 deletions(-) create mode 100644 agent_shared/src/custom_queries.rs diff --git a/agent_holder/src/credential/queries/all_credentials.rs b/agent_holder/src/credential/queries/all_credentials.rs index dbdb764c..48000182 100644 --- a/agent_holder/src/credential/queries/all_credentials.rs +++ b/agent_holder/src/credential/queries/all_credentials.rs @@ -1,83 +1,12 @@ -use crate::credential::queries::{Credential, CustomQuery, ViewRepository}; -use async_trait::async_trait; -use cqrs_es::{ - persist::{PersistenceError, ViewContext}, - EventEnvelope, Query, View, -}; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use std::{collections::HashMap, marker::PhantomData}; - use super::CredentialView; - -const VIEW_ID: &str = "all_credentials"; - -/// A custom query trait for the Credential aggregate. This query is used to update the `AllCredentialsView`. -pub struct AllCredentialsQuery -where - R: ViewRepository, - V: View, -{ - view_repository: Arc, - _phantom: PhantomData, -} - -impl AllCredentialsQuery -where - R: ViewRepository, - V: View, -{ - pub fn new(view_repository: Arc) -> Self { - AllCredentialsQuery { - view_repository, - _phantom: PhantomData, - } - } -} - -#[async_trait] -impl Query for AllCredentialsQuery -where - R: ViewRepository, - V: View, -{ - // The `dispatch` method is called by the `CqrsFramework` when an event is published. By default `cqrs` will use the - // `aggregate_id` as the `view_id` when calling the `dispatch` method. We override this behavior by using the - // `VIEW_ID` constant as the `view_id`. - async fn dispatch(&self, _view_id: &str, events: &[EventEnvelope]) { - self.apply_events(VIEW_ID, events).await.ok(); - } -} - -#[async_trait] -impl CustomQuery for AllCredentialsQuery -where - R: ViewRepository, - V: View, -{ - async fn load_mut(&self, view_id: String) -> Result<(V, ViewContext), PersistenceError> { - match self.view_repository.load_with_context(&view_id).await? { - None => { - let view_context = ViewContext::new(view_id, 0); - Ok((Default::default(), view_context)) - } - Some((view, context)) => Ok((view, context)), - } - } - - async fn apply_events(&self, view_id: &str, events: &[EventEnvelope]) -> Result<(), PersistenceError> { - for event in events { - let (mut view, view_context) = self.load_mut(view_id.to_string()).await?; - - view.update(event); - self.view_repository.update_view(view, view_context).await?; - } - Ok(()) - } -} +use crate::credential::queries::Credential; +use cqrs_es::{EventEnvelope, View}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct AllCredentialsView { + #[serde(flatten)] pub credentials: HashMap, } diff --git a/agent_holder/src/credential/queries/mod.rs b/agent_holder/src/credential/queries/mod.rs index f9caebf9..007f0255 100644 --- a/agent_holder/src/credential/queries/mod.rs +++ b/agent_holder/src/credential/queries/mod.rs @@ -2,26 +2,9 @@ pub mod all_credentials; use super::event::CredentialEvent; use crate::credential::aggregate::Credential; -use async_trait::async_trait; -use cqrs_es::{ - persist::{PersistenceError, ViewContext, ViewRepository}, - EventEnvelope, Query, View, -}; +use cqrs_es::{EventEnvelope, View}; use serde::{Deserialize, Serialize}; -/// A custom query trait for the Credential aggregate. This trait is used to define custom queries for the Credential aggregate -/// that do not make use of `GenericQuery`. -#[async_trait] -pub trait CustomQuery: Query -where - R: ViewRepository, - V: View, -{ - async fn load_mut(&self, view_id: String) -> Result<(V, ViewContext), PersistenceError>; - - async fn apply_events(&self, view_id: &str, events: &[EventEnvelope]) -> Result<(), PersistenceError>; -} - #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct CredentialView { pub credential_id: Option, diff --git a/agent_holder/src/offer/queries/all_offers.rs b/agent_holder/src/offer/queries/all_offers.rs index a12a4ec4..b9696bba 100644 --- a/agent_holder/src/offer/queries/all_offers.rs +++ b/agent_holder/src/offer/queries/all_offers.rs @@ -1,83 +1,12 @@ -use crate::offer::queries::{CustomQuery, Offer, ViewRepository}; -use async_trait::async_trait; -use cqrs_es::{ - persist::{PersistenceError, ViewContext}, - EventEnvelope, Query, View, -}; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use std::{collections::HashMap, marker::PhantomData}; - use super::OfferView; - -const VIEW_ID: &str = "all_offers"; - -/// A custom query trait for the Offer aggregate. This query is used to update the `AllOffersView`. -pub struct AllOffersQuery -where - R: ViewRepository, - V: View, -{ - view_repository: Arc, - _phantom: PhantomData, -} - -impl AllOffersQuery -where - R: ViewRepository, - V: View, -{ - pub fn new(view_repository: Arc) -> Self { - AllOffersQuery { - view_repository, - _phantom: PhantomData, - } - } -} - -#[async_trait] -impl Query for AllOffersQuery -where - R: ViewRepository, - V: View, -{ - // The `dispatch` method is called by the `CqrsFramework` when an event is published. By default `cqrs` will use the - // `aggregate_id` as the `view_id` when calling the `dispatch` method. We override this behavior by using the - // `VIEW_ID` constant as the `view_id`. - async fn dispatch(&self, _view_id: &str, events: &[EventEnvelope]) { - self.apply_events(VIEW_ID, events).await.ok(); - } -} - -#[async_trait] -impl CustomQuery for AllOffersQuery -where - R: ViewRepository, - V: View, -{ - async fn load_mut(&self, view_id: String) -> Result<(V, ViewContext), PersistenceError> { - match self.view_repository.load_with_context(&view_id).await? { - None => { - let view_context = ViewContext::new(view_id, 0); - Ok((Default::default(), view_context)) - } - Some((view, context)) => Ok((view, context)), - } - } - - async fn apply_events(&self, view_id: &str, events: &[EventEnvelope]) -> Result<(), PersistenceError> { - for event in events { - let (mut view, view_context) = self.load_mut(view_id.to_string()).await?; - - view.update(event); - self.view_repository.update_view(view, view_context).await?; - } - Ok(()) - } -} +use crate::offer::queries::Offer; +use cqrs_es::{EventEnvelope, View}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct AllOffersView { + #[serde(flatten)] pub offers: HashMap, } diff --git a/agent_holder/src/offer/queries/mod.rs b/agent_holder/src/offer/queries/mod.rs index 3a36bfb2..ad13bde1 100644 --- a/agent_holder/src/offer/queries/mod.rs +++ b/agent_holder/src/offer/queries/mod.rs @@ -1,34 +1,14 @@ pub mod all_offers; -use std::collections::HashMap; - -use async_trait::async_trait; -use cqrs_es::{ - persist::{PersistenceError, ViewContext, ViewRepository}, - EventEnvelope, Query, View, -}; +use super::aggregate::Status; +use crate::offer::aggregate::Offer; +use cqrs_es::{EventEnvelope, View}; use oid4vci::{ credential_issuer::credential_configurations_supported::CredentialConfigurationsSupportedObject, credential_offer::CredentialOfferParameters, token_response::TokenResponse, }; use serde::{Deserialize, Serialize}; - -use crate::offer::aggregate::Offer; - -use super::aggregate::Status; - -/// A custom query trait for the Offer aggregate. This trait is used to define custom queries for the Offer aggregate -/// that do not make use of `GenericQuery`. -#[async_trait] -pub trait CustomQuery: Query -where - R: ViewRepository, - V: View, -{ - async fn load_mut(&self, view_id: String) -> Result<(V, ViewContext), PersistenceError>; - - async fn apply_events(&self, view_id: &str, events: &[EventEnvelope]) -> Result<(), PersistenceError>; -} +use std::collections::HashMap; #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct OfferView { diff --git a/agent_issuance/src/offer/queries/access_token.rs b/agent_issuance/src/offer/queries/access_token.rs index d25935f5..0b33ed95 100644 --- a/agent_issuance/src/offer/queries/access_token.rs +++ b/agent_issuance/src/offer/queries/access_token.rs @@ -1,4 +1,5 @@ -use crate::offer::queries::{CustomQuery, Offer, OfferEvent, ViewRepository}; +use crate::offer::queries::{Offer, OfferEvent, ViewRepository}; +use agent_shared::custom_queries::CustomQuery; use async_trait::async_trait; use cqrs_es::{ persist::{PersistenceError, ViewContext}, @@ -43,7 +44,7 @@ where } #[async_trait] -impl CustomQuery for AccessTokenQuery +impl CustomQuery for AccessTokenQuery where R: ViewRepository, V: View, diff --git a/agent_issuance/src/offer/queries/mod.rs b/agent_issuance/src/offer/queries/mod.rs index 24be5166..806f7240 100644 --- a/agent_issuance/src/offer/queries/mod.rs +++ b/agent_issuance/src/offer/queries/mod.rs @@ -1,33 +1,14 @@ pub mod access_token; pub mod pre_authorized_code; -use async_trait::async_trait; -use cqrs_es::{ - persist::{PersistenceError, ViewContext, ViewRepository}, - EventEnvelope, Query, View, -}; +use super::event::OfferEvent; +use crate::offer::aggregate::Offer; +use cqrs_es::{persist::ViewRepository, EventEnvelope, View}; use oid4vci::{ credential_offer::CredentialOffer, credential_response::CredentialResponse, token_response::TokenResponse, }; use serde::{Deserialize, Serialize}; -use crate::offer::aggregate::Offer; - -use super::event::OfferEvent; - -/// A custom query trait for the Offer aggregate. This trait is used to define custom queries for the Offer aggregate -/// that do not make use of `GenericQuery`. -#[async_trait] -pub trait CustomQuery: Query -where - R: ViewRepository, - V: View, -{ - async fn load_mut(&self, view_id: String) -> Result<(V, ViewContext), PersistenceError>; - - async fn apply_events(&self, view_id: &str, events: &[EventEnvelope]) -> Result<(), PersistenceError>; -} - #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct OfferView { pub credential_offer: Option, diff --git a/agent_issuance/src/offer/queries/pre_authorized_code.rs b/agent_issuance/src/offer/queries/pre_authorized_code.rs index 2f96bd13..395f873e 100644 --- a/agent_issuance/src/offer/queries/pre_authorized_code.rs +++ b/agent_issuance/src/offer/queries/pre_authorized_code.rs @@ -1,4 +1,5 @@ -use crate::offer::queries::{CustomQuery, Offer, OfferEvent, ViewRepository}; +use crate::offer::queries::{Offer, OfferEvent, ViewRepository}; +use agent_shared::custom_queries::CustomQuery; use async_trait::async_trait; use cqrs_es::{ persist::{PersistenceError, ViewContext}, @@ -43,7 +44,7 @@ where } #[async_trait] -impl CustomQuery for PreAuthorizedCodeQuery +impl CustomQuery for PreAuthorizedCodeQuery where R: ViewRepository, V: View, diff --git a/agent_shared/src/custom_queries.rs b/agent_shared/src/custom_queries.rs new file mode 100644 index 00000000..f327e0a5 --- /dev/null +++ b/agent_shared/src/custom_queries.rs @@ -0,0 +1,88 @@ +use async_trait::async_trait; +use cqrs_es::{ + persist::{PersistenceError, ViewContext, ViewRepository}, + Aggregate, EventEnvelope, Query, View, +}; +use std::marker::PhantomData; +use std::sync::Arc; + +/// A custom query trait. This trait is used to define custom queries for the Aggregates that do not make use of +/// `GenericQuery`. +#[async_trait] +pub trait CustomQuery: Query +where + R: ViewRepository, + V: View, + A: Aggregate, +{ + async fn load_mut(&self, view_id: String) -> Result<(V, ViewContext), PersistenceError>; + + async fn apply_events(&self, view_id: &str, events: &[EventEnvelope]) -> Result<(), PersistenceError>; +} + +/// A struct that lists all the instances of an `Aggregate`. +pub struct ListAllQuery +where + R: ViewRepository, + V: View, + A: Aggregate, +{ + view_id: String, + view_repository: Arc, + _phantom: PhantomData<(V, A)>, +} + +impl ListAllQuery +where + R: ViewRepository, + V: View, + A: Aggregate, +{ + pub fn new(view_repository: Arc, view_id: &str) -> Self { + ListAllQuery { + view_id: view_id.to_string(), + view_repository, + _phantom: PhantomData, + } + } +} + +#[async_trait] +impl Query for ListAllQuery +where + R: ViewRepository, + V: View, + A: Aggregate, +{ + async fn dispatch(&self, _view_id: &str, events: &[EventEnvelope]) { + self.apply_events(&self.view_id, events).await.ok(); + } +} + +#[async_trait] +impl CustomQuery for ListAllQuery +where + R: ViewRepository, + V: View, + A: Aggregate, +{ + async fn load_mut(&self, view_id: String) -> Result<(V, ViewContext), PersistenceError> { + match self.view_repository.load_with_context(&view_id).await? { + None => { + let view_context = ViewContext::new(view_id, 0); + Ok((Default::default(), view_context)) + } + Some((view, context)) => Ok((view, context)), + } + } + + async fn apply_events(&self, view_id: &str, events: &[EventEnvelope]) -> Result<(), PersistenceError> { + for event in events { + let (mut view, view_context) = self.load_mut(view_id.to_string()).await?; + + view.update(event); + self.view_repository.update_view(view, view_context).await?; + } + Ok(()) + } +} diff --git a/agent_shared/src/lib.rs b/agent_shared/src/lib.rs index e678f5bf..6a7d89ed 100644 --- a/agent_shared/src/lib.rs +++ b/agent_shared/src/lib.rs @@ -1,5 +1,6 @@ pub mod application_state; pub mod config; +pub mod custom_queries; pub mod domain_linkage; pub mod error; pub mod generic_query; diff --git a/agent_store/src/in_memory.rs b/agent_store/src/in_memory.rs index 44db81e5..5e5fef7d 100644 --- a/agent_store/src/in_memory.rs +++ b/agent_store/src/in_memory.rs @@ -1,7 +1,5 @@ -use agent_holder::{ - credential::queries::all_credentials::AllCredentialsQuery, offer::queries::all_offers::AllOffersQuery, - services::HolderServices, state::HolderState, -}; +use crate::{partition_event_publishers, EventPublisher}; +use agent_holder::{services::HolderServices, state::HolderState}; use agent_issuance::{ offer::{ aggregate::Offer, @@ -14,7 +12,7 @@ use agent_issuance::{ state::{IssuanceState, ViewRepositories}, SimpleLoggingQuery, }; -use agent_shared::{application_state::Command, generic_query::generic_query}; +use agent_shared::{application_state::Command, custom_queries::ListAllQuery, generic_query::generic_query}; use agent_verification::{services::VerificationServices, state::VerificationState}; use async_trait::async_trait; use cqrs_es::{ @@ -25,8 +23,6 @@ use cqrs_es::{ use std::{collections::HashMap, sync::Arc}; use tokio::sync::Mutex; -use crate::{partition_event_publishers, EventPublisher}; - #[derive(Default)] struct MemRepository, A: Aggregate> { pub map: Mutex>, @@ -188,8 +184,8 @@ pub async fn holder_state( let all_offers = Arc::new(MemRepository::default()); // Create custom-queries for the offer aggregate. - let all_credentials_query = AllCredentialsQuery::new(all_credentials.clone()); - let all_offers_query = AllOffersQuery::new(all_offers.clone()); + let all_credentials_query = ListAllQuery::new(all_credentials.clone(), "all_credentials"); + let all_offers_query = ListAllQuery::new(all_offers.clone(), "all_offers"); // Partition the event_publishers into the different aggregates. let (_, _, _, credential_event_publishers, offer_event_publishers, _, _) = diff --git a/agent_store/src/postgres.rs b/agent_store/src/postgres.rs index 4c6adb97..5a91de7e 100644 --- a/agent_store/src/postgres.rs +++ b/agent_store/src/postgres.rs @@ -1,14 +1,14 @@ -use agent_holder::{ - credential::queries::all_credentials::AllCredentialsQuery, offer::queries::all_offers::AllOffersQuery, - services::HolderServices, state::HolderState, -}; +use crate::{partition_event_publishers, EventPublisher}; +use agent_holder::{services::HolderServices, state::HolderState}; use agent_issuance::{ offer::queries::{access_token::AccessTokenQuery, pre_authorized_code::PreAuthorizedCodeQuery}, services::IssuanceServices, state::{CommandHandlers, IssuanceState, ViewRepositories}, SimpleLoggingQuery, }; -use agent_shared::{application_state::Command, config::config, generic_query::generic_query}; +use agent_shared::{ + application_state::Command, config::config, custom_queries::ListAllQuery, generic_query::generic_query, +}; use agent_verification::{services::VerificationServices, state::VerificationState}; use async_trait::async_trait; use cqrs_es::{Aggregate, Query}; @@ -16,8 +16,6 @@ use postgres_es::{default_postgress_pool, PostgresCqrs, PostgresViewRepository}; use sqlx::{Pool, Postgres}; use std::{collections::HashMap, sync::Arc}; -use crate::{partition_event_publishers, EventPublisher}; - struct AggregateHandler where A: Aggregate, @@ -148,8 +146,8 @@ pub async fn holder_state( let all_offers = Arc::new(PostgresViewRepository::new("all_offers", pool.clone())); // Create custom-queries for the offer aggregate. - let all_credentials_query = AllCredentialsQuery::new(all_credentials.clone()); - let all_offers_query = AllOffersQuery::new(all_offers.clone()); + let all_credentials_query = ListAllQuery::new(all_credentials.clone(), "all_credentials"); + let all_offers_query = ListAllQuery::new(all_offers.clone(), "all_offers"); // Partition the event_publishers into the different aggregates. let (_, _, _, credential_event_publishers, offer_event_publishers, _, _) = From 4c39ac710deea9045d06f7addc4e982c9b93b946 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 30 Aug 2024 15:19:25 +0200 Subject: [PATCH 21/31] fix: add Into for SupportedDidMethod --- agent_holder/src/services.rs | 11 +++++------ agent_shared/src/config.rs | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/agent_holder/src/services.rs b/agent_holder/src/services.rs index 2958b6dd..17668375 100644 --- a/agent_holder/src/services.rs +++ b/agent_holder/src/services.rs @@ -3,7 +3,7 @@ use agent_shared::config::{config, get_all_enabled_did_methods, get_preferred_di use jsonwebtoken::Algorithm; use oid4vc_core::{Subject, SubjectSyntaxType}; use oid4vci::Wallet; -use std::{str::FromStr, sync::Arc}; +use std::sync::Arc; /// Holder services. This struct is used to sign credentials and validate credential requests. pub struct HolderServices { @@ -32,17 +32,16 @@ impl Service for HolderServices { } }); - let supported_subject_syntax_types = enabled_did_methods - .into_iter() - .map(|method| SubjectSyntaxType::from_str(&method.to_string()).unwrap()) - .collect(); + let supported_subject_syntax_types: Vec = + enabled_did_methods.into_iter().map(Into::into).collect(); let wallet = Wallet::new( holder.clone(), supported_subject_syntax_types, signing_algorithms_supported, ) - .unwrap(); + // TODO: make `Wallet::new` return `Wallet` instead of `Result` + .expect("Failed to create wallet"); Self { holder, wallet } } diff --git a/agent_shared/src/config.rs b/agent_shared/src/config.rs index 2436f87a..8e631f60 100644 --- a/agent_shared/src/config.rs +++ b/agent_shared/src/config.rs @@ -1,4 +1,5 @@ use config::ConfigError; +use oid4vc_core::SubjectSyntaxType; use oid4vci::credential_format_profiles::{CredentialFormats, WithParameters}; use oid4vp::ClaimFormatDesignation; use once_cell::sync::Lazy; @@ -8,6 +9,7 @@ use std::{ collections::HashMap, sync::{RwLock, RwLockReadGuard}, }; +use strum::VariantArray; use tracing::{debug, info}; use url::Url; @@ -179,7 +181,18 @@ pub enum AuthorizationRequestEvent { /// assert_eq!(supported_did_method.to_string(), "did:jwk"); /// ``` #[derive( - Debug, Deserialize, Clone, Eq, PartialEq, Hash, strum::EnumString, strum::Display, SerializeDisplay, Ord, PartialOrd, + Debug, + Deserialize, + Clone, + Eq, + PartialEq, + Hash, + strum::EnumString, + strum::Display, + SerializeDisplay, + Ord, + PartialOrd, + VariantArray, )] pub enum SupportedDidMethod { #[serde(alias = "did_jwk", rename = "did_jwk")] @@ -202,6 +215,12 @@ pub enum SupportedDidMethod { IotaRms, } +impl Into for SupportedDidMethod { + fn into(self) -> SubjectSyntaxType { + SubjectSyntaxType::try_from(self.to_string().as_str()).expect("convertion into `SubjectSyntaxType` failed") + } +} + /// Generic options that add an "enabled" field and a "preferred" field (optional) to a configuration. #[derive(Debug, Deserialize, Default, Clone)] pub struct ToggleOptions { @@ -341,3 +360,15 @@ pub fn get_preferred_signing_algorithm() -> jsonwebtoken::Algorithm { .cloned() .expect("Please set a signing algorithm as `preferred` in the configuration") } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn all_supported_did_methods_can_be_converted_into_subject_syntax_type() { + for variant in SupportedDidMethod::VARIANTS { + let _subject_syntax_type: SubjectSyntaxType = variant.clone().into(); + } + } +} From 29f25dac8db1b6b566b94b6693424e6285204cab Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 30 Aug 2024 15:20:21 +0200 Subject: [PATCH 22/31] fix: return 200 OK when list is empty --- agent_api_rest/src/holder/holder/credentials/mod.rs | 4 ++-- agent_api_rest/src/holder/holder/offers/mod.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/agent_api_rest/src/holder/holder/credentials/mod.rs b/agent_api_rest/src/holder/holder/credentials/mod.rs index 806e96a1..5e91880c 100644 --- a/agent_api_rest/src/holder/holder/credentials/mod.rs +++ b/agent_api_rest/src/holder/holder/credentials/mod.rs @@ -6,13 +6,13 @@ use axum::{ Json, }; use hyper::StatusCode; +use serde_json::json; #[axum_macros::debug_handler] pub(crate) async fn credentials(State(state): State) -> Response { - // TODO: Add extension that allows for selecting all credentials. match query_handler("all_credentials", &state.query.all_credentials).await { Ok(Some(offer_view)) => (StatusCode::OK, Json(offer_view)).into_response(), - Ok(None) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + Ok(None) => (StatusCode::OK, Json(json!({}))).into_response(), _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } } diff --git a/agent_api_rest/src/holder/holder/offers/mod.rs b/agent_api_rest/src/holder/holder/offers/mod.rs index a4fb976f..c513aecd 100644 --- a/agent_api_rest/src/holder/holder/offers/mod.rs +++ b/agent_api_rest/src/holder/holder/offers/mod.rs @@ -9,13 +9,13 @@ use axum::{ Json, }; use hyper::StatusCode; +use serde_json::json; #[axum_macros::debug_handler] pub(crate) async fn offers(State(state): State) -> Response { - // TODO: Add extension that allows for selecting all offers. match query_handler("all_offers", &state.query.all_offers).await { Ok(Some(offer_view)) => (StatusCode::OK, Json(offer_view)).into_response(), - Ok(None) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + Ok(None) => (StatusCode::OK, Json(json!({}))).into_response(), _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } } From e08a045c1c36a63c68700e0d2426e172d5d8421a Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 30 Aug 2024 15:21:13 +0200 Subject: [PATCH 23/31] refactor: clean up code --- .../src/holder/holder/offers/accept.rs | 24 +++++++++++------- agent_api_rest/src/holder/mod.rs | 7 +++--- .../issuance/credential_issuer/credential.rs | 3 +-- agent_holder/src/credential/README.md | 5 ++-- agent_holder/src/credential/entity.rs | 6 ----- agent_holder/src/credential/error.rs | 20 +-------------- agent_holder/src/credential/mod.rs | 1 - agent_holder/src/offer/README.md | 11 ++++---- agent_holder/src/offer/aggregate.rs | 25 ++++++++++++++++--- agent_holder/src/offer/error.rs | 12 +++------ agent_holder/src/offer/event.rs | 6 ++--- agent_issuance/src/credential/aggregate.rs | 12 ++++----- agent_secret_manager/src/service.rs | 1 + 13 files changed, 61 insertions(+), 72 deletions(-) delete mode 100644 agent_holder/src/credential/entity.rs diff --git a/agent_api_rest/src/holder/holder/offers/accept.rs b/agent_api_rest/src/holder/holder/offers/accept.rs index e6f1446e..0cf1a0bb 100644 --- a/agent_api_rest/src/holder/holder/offers/accept.rs +++ b/agent_api_rest/src/holder/holder/offers/accept.rs @@ -17,22 +17,28 @@ pub(crate) async fn accept(State(state): State, Path(offer_id): Pat // Requests and Responses. // Furthermore, the to be implemented Application Layer should be kept very thin as well. See: https://github.com/impierce/ssi-agent/issues/114 - let command = OfferCommand::AcceptCredentialOffer { - offer_id: offer_id.clone(), - }; + // Accept the Credential Offer if it exists + match query_handler(&offer_id, &state.query.offer).await { + Ok(Some(OfferView { .. })) => { + let command = OfferCommand::AcceptCredentialOffer { + offer_id: offer_id.clone(), + }; - // Add the Credential Offer to the state. - if command_handler(&offer_id, &state.command.offer, command).await.is_err() { - // TODO: add better Error responses. This needs to be done properly in all endpoints once - // https://github.com/impierce/openid4vc/issues/78 is fixed. - return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + if command_handler(&offer_id, &state.command.offer, command).await.is_err() { + // TODO: add better Error responses. This needs to be done properly in all endpoints once + // https://github.com/impierce/openid4vc/issues/78 is fixed. + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + } + Ok(None) => return StatusCode::NOT_FOUND.into_response(), + _ => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), } let command = OfferCommand::SendCredentialRequest { offer_id: offer_id.clone(), }; - // Add the Credential Offer to the state. + // Send the Credential Request if command_handler(&offer_id, &state.command.offer, command).await.is_err() { // TODO: add better Error responses. This needs to be done properly in all endpoints once // https://github.com/impierce/openid4vc/issues/78 is fixed. diff --git a/agent_api_rest/src/holder/mod.rs b/agent_api_rest/src/holder/mod.rs index 93f06f86..7ea56ad7 100644 --- a/agent_api_rest/src/holder/mod.rs +++ b/agent_api_rest/src/holder/mod.rs @@ -3,15 +3,14 @@ pub mod holder; pub mod openid4vci; -use agent_holder::state::HolderState; -use axum::routing::get; -use axum::{routing::post, Router}; - use crate::holder::holder::{ credentials::credentials, offers::{accept::accept, reject::reject, *}, }; use crate::API_VERSION; +use agent_holder::state::HolderState; +use axum::routing::get; +use axum::{routing::post, Router}; pub fn router(holder_state: HolderState) -> Router { Router::new() diff --git a/agent_api_rest/src/issuance/credential_issuer/credential.rs b/agent_api_rest/src/issuance/credential_issuer/credential.rs index 39864600..c0d43200 100644 --- a/agent_api_rest/src/issuance/credential_issuer/credential.rs +++ b/agent_api_rest/src/issuance/credential_issuer/credential.rs @@ -156,6 +156,7 @@ mod tests { }; use agent_event_publisher_http::EventPublisherHttp; use agent_issuance::{offer::event::OfferEvent, startup_commands::startup_commands, state::initialize}; + use agent_secret_manager::service::Service; use agent_shared::config::{set_config, Events}; use agent_store::{in_memory, EventPublisher}; use axum::{ @@ -276,8 +277,6 @@ mod tests { #[case] is_self_signed: bool, #[case] delay: u64, ) { - use agent_secret_manager::service::Service; - let (external_server, issuance_event_publishers) = if with_external_server { let external_server = MockServer::start().await; diff --git a/agent_holder/src/credential/README.md b/agent_holder/src/credential/README.md index ce77f83b..78cc0876 100644 --- a/agent_holder/src/credential/README.md +++ b/agent_holder/src/credential/README.md @@ -2,5 +2,6 @@ This aggregate is defined by: -- credential data -- a format (such as: _Open Badge 3.0_) +- credential_id +- offer_id +- credential diff --git a/agent_holder/src/credential/entity.rs b/agent_holder/src/credential/entity.rs deleted file mode 100644 index 432325fb..00000000 --- a/agent_holder/src/credential/entity.rs +++ /dev/null @@ -1,6 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] -pub struct Data { - pub raw: serde_json::Value, -} diff --git a/agent_holder/src/credential/error.rs b/agent_holder/src/credential/error.rs index c03f6492..df235841 100644 --- a/agent_holder/src/credential/error.rs +++ b/agent_holder/src/credential/error.rs @@ -1,22 +1,4 @@ use thiserror::Error; #[derive(Error, Debug)] -pub enum CredentialError { - #[error("Credential must be an object")] - InvalidCredentialError, - - #[error("This Credential format it not supported")] - UnsupportedCredentialFormat, - - #[error("The `credentialSubject` parameter is missing")] - MissingCredentialSubjectError, - - #[error("The supplied `credentialSubject` is invalid: {0}")] - InvalidCredentialSubjectError(String), - - #[error("The verifiable credential is invalid: {0}")] - InvalidVerifiableCredentialError(String), - - #[error("Could not find any data to be signed")] - MissingCredentialDataError, -} +pub enum CredentialError {} diff --git a/agent_holder/src/credential/mod.rs b/agent_holder/src/credential/mod.rs index 5c6981d1..7d8a943f 100644 --- a/agent_holder/src/credential/mod.rs +++ b/agent_holder/src/credential/mod.rs @@ -1,6 +1,5 @@ pub mod aggregate; pub mod command; -pub mod entity; pub mod error; pub mod event; pub mod queries; diff --git a/agent_holder/src/offer/README.md b/agent_holder/src/offer/README.md index 4c0e60ac..f8386aed 100644 --- a/agent_holder/src/offer/README.md +++ b/agent_holder/src/offer/README.md @@ -1,10 +1,9 @@ # Offer -This aggregate holds everything related to an offer of a credential to a subject: +This aggregate holds everything related to a credential offer: -- credential_ids -- form_url_encoded_credential_offer -- pre_authorized_code +- credential_offer +- status +- credential_configurations - token_response -- access_token -- credential_response +- credentials diff --git a/agent_holder/src/offer/aggregate.rs b/agent_holder/src/offer/aggregate.rs index 6edb89fc..100b2f42 100644 --- a/agent_holder/src/offer/aggregate.rs +++ b/agent_holder/src/offer/aggregate.rs @@ -48,6 +48,7 @@ impl Aggregate for Offer { async fn handle(&self, command: Self::Command, services: &Self::Services) -> Result, Self::Error> { use OfferCommand::*; + use OfferError::*; use OfferEvent::*; info!("Handling command: {:?}", command); @@ -92,6 +93,11 @@ impl Aggregate for Offer { }]) } AcceptCredentialOffer { offer_id } => { + // TODO: should we 'do nothing' or log a `warn!` message instead of returning an error? + if self.status != Status::Pending { + return Err(CredentialOfferStatusNotPendingError); + } + let wallet = &services.wallet; let credential_issuer_url = self.credential_offer.as_ref().unwrap().credential_issuer.clone(); @@ -135,6 +141,10 @@ impl Aggregate for Offer { ]) } SendCredentialRequest { offer_id } => { + if self.status != Status::Accepted { + return Err(CredentialOfferStatusNotAcceptedError); + } + let wallet = &services.wallet; let credential_issuer_url = self.credential_offer.as_ref().unwrap().credential_issuer.clone(); @@ -193,10 +203,17 @@ impl Aggregate for Offer { credentials, }]) } - RejectCredentialOffer { offer_id } => Ok(vec![CredentialOfferRejected { - offer_id, - status: Status::Rejected, - }]), + RejectCredentialOffer { offer_id } => { + // TODO: should we 'do nothing' or log a `warn!` message instead of returning an error? + if self.status != Status::Pending { + return Err(CredentialOfferStatusNotPendingError); + } + + Ok(vec![CredentialOfferRejected { + offer_id, + status: Status::Rejected, + }]) + } } } diff --git a/agent_holder/src/offer/error.rs b/agent_holder/src/offer/error.rs index 3cd038e7..7c44918a 100644 --- a/agent_holder/src/offer/error.rs +++ b/agent_holder/src/offer/error.rs @@ -2,12 +2,8 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum OfferError { - #[error("Credential is missing")] - MissingCredentialError, - #[error("Missing `Proof` in Credential Request")] - MissingProofError, - #[error("Invalid `Proof` in Credential Request")] - InvalidProofError(String), - #[error("Missing `iss` claim in `Proof`")] - MissingProofIssuerError, + #[error("The Credential Offer has already been accepted and cannot be rejected anymore")] + CredentialOfferStatusNotPendingError, + #[error("The Credential Offer has not been accepted yet")] + CredentialOfferStatusNotAcceptedError, } diff --git a/agent_holder/src/offer/event.rs b/agent_holder/src/offer/event.rs index da3d6281..4db40468 100644 --- a/agent_holder/src/offer/event.rs +++ b/agent_holder/src/offer/event.rs @@ -1,13 +1,11 @@ -use std::collections::HashMap; - +use super::aggregate::Status; use cqrs_es::DomainEvent; use oid4vci::{ credential_issuer::credential_configurations_supported::CredentialConfigurationsSupportedObject, credential_offer::CredentialOfferParameters, token_response::TokenResponse, }; use serde::{Deserialize, Serialize}; - -use super::aggregate::Status; +use std::collections::HashMap; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum OfferEvent { diff --git a/agent_issuance/src/credential/aggregate.rs b/agent_issuance/src/credential/aggregate.rs index 2e24cc07..b64ee7da 100644 --- a/agent_issuance/src/credential/aggregate.rs +++ b/agent_issuance/src/credential/aggregate.rs @@ -1,3 +1,8 @@ +use super::entity::Data; +use crate::credential::command::CredentialCommand; +use crate::credential::error::CredentialError::{self}; +use crate::credential::event::CredentialEvent; +use crate::services::IssuanceServices; use agent_shared::config::{config, get_preferred_did_method, get_preferred_signing_algorithm}; use async_trait::async_trait; use cqrs_es::Aggregate; @@ -23,13 +28,6 @@ use types_ob_v3::prelude::{ ProfileBuilder, }; -use crate::credential::command::CredentialCommand; -use crate::credential::error::CredentialError::{self}; -use crate::credential::event::CredentialEvent; -use crate::services::IssuanceServices; - -use super::entity::Data; - #[derive(Debug, Clone, Serialize, Deserialize, Default, Derivative)] #[derivative(PartialEq)] pub struct Credential { diff --git a/agent_secret_manager/src/service.rs b/agent_secret_manager/src/service.rs index 9c9a1748..a96a776b 100644 --- a/agent_secret_manager/src/service.rs +++ b/agent_secret_manager/src/service.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +/// Conventience trait for Services like `IssuanceServices`, `HolderServices`, and `VerifierServices`. pub trait Service { fn new(subject: Arc) -> Self; From f307da3eb4b50d939c9eaf83607c8cf7981e0d8f Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 30 Aug 2024 15:50:50 +0200 Subject: [PATCH 24/31] fix: Fix error handling for the Offer aggregate --- agent_holder/src/offer/aggregate.rs | 67 ++++++++++++++++------------- agent_holder/src/offer/error.rs | 26 +++++++++++ 2 files changed, 64 insertions(+), 29 deletions(-) diff --git a/agent_holder/src/offer/aggregate.rs b/agent_holder/src/offer/aggregate.rs index 100b2f42..47239bdf 100644 --- a/agent_holder/src/offer/aggregate.rs +++ b/agent_holder/src/offer/aggregate.rs @@ -65,7 +65,7 @@ impl Aggregate for Offer { .wallet .get_credential_offer(credential_offer_uri) .await - .unwrap(), + .map_err(|_| CredentialOfferByReferenceRetrievalError)?, CredentialOffer::CredentialOffer(credential_offer) => *credential_offer, }; @@ -76,7 +76,7 @@ impl Aggregate for Offer { let credential_issuer_metadata = wallet .get_credential_issuer_metadata(credential_issuer_url.clone()) .await - .unwrap(); + .map_err(|_| CredentialIssuerMetadataRetrievalError)?; let credential_configurations: HashMap = credential_issuer_metadata @@ -100,32 +100,40 @@ impl Aggregate for Offer { let wallet = &services.wallet; - let credential_issuer_url = self.credential_offer.as_ref().unwrap().credential_issuer.clone(); + let credential_offer = self.credential_offer.as_ref().ok_or(MissingCredentialOfferError)?; + + let credential_issuer_url = credential_offer.credential_issuer.clone(); // Get the authorization server metadata. let authorization_server_metadata = wallet .get_authorization_server_metadata(credential_issuer_url.clone()) .await - .unwrap(); + .map_err(|_| AuthorizationServerMetadataRetrievalError)?; // Create a token request with grant_type `pre_authorized_code`. - let token_request = match self.credential_offer.as_ref().unwrap().grants.clone() { + let token_request = match credential_offer.grants.clone() { Some(Grants { - pre_authorized_code, .. + pre_authorized_code: Some(pre_authorized_code), + .. }) => TokenRequest::PreAuthorizedCode { - pre_authorized_code: pre_authorized_code.unwrap().pre_authorized_code, + pre_authorized_code: pre_authorized_code.pre_authorized_code, tx_code: None, }, - None => unreachable!(), + _ => return Err(MissingPreAuthorizedCodeError), }; info!("token_request: {:?}", token_request); // Get an access token. let token_response = wallet - .get_access_token(authorization_server_metadata.token_endpoint.unwrap(), token_request) + .get_access_token( + authorization_server_metadata + .token_endpoint + .ok_or(MissingTokenEndpointError)?, + token_request, + ) .await - .unwrap(); + .map_err(|_| TokenResponseError)?; info!("token_response: {:?}", token_response); @@ -147,51 +155,52 @@ impl Aggregate for Offer { let wallet = &services.wallet; - let credential_issuer_url = self.credential_offer.as_ref().unwrap().credential_issuer.clone(); + let credential_offer = self.credential_offer.as_ref().ok_or(MissingCredentialOfferError)?; + + let credential_issuer_url = credential_offer.credential_issuer.clone(); // Get an access token. - let token_response = self.token_response.as_ref().unwrap().clone(); + let token_response = self.token_response.as_ref().ok_or(MissingTokenResponseError)?.clone(); - let credential_configuration_ids = self - .credential_offer - .as_ref() - .unwrap() - .credential_configuration_ids - .clone(); + let credential_configuration_ids = credential_offer.credential_configuration_ids.clone(); // Get the credential issuer metadata. let credential_issuer_metadata = wallet .get_credential_issuer_metadata(credential_issuer_url.clone()) .await - .unwrap(); + .map_err(|_| CredentialIssuerMetadataRetrievalError)?; + + let credential_configurations = self + .credential_configurations + .as_ref() + .ok_or(MissingCredentialConfigurationsError)?; let credentials: Vec = match credential_configuration_ids.len() { 0 => vec![], 1 => { - let credential_configuration_id = credential_configuration_ids[0].clone(); + let credential_configuration_id = &credential_configuration_ids[0]; - let credential_configuration = self - .credential_configurations - .as_ref() - .unwrap() - .get(&credential_configuration_id) - .unwrap(); + let credential_configuration = credential_configurations + .get(credential_configuration_id) + .ok_or(MissingCredentialConfigurationError)?; // Get the credential. let credential_response = wallet .get_credential(credential_issuer_metadata, &token_response, credential_configuration) .await - .unwrap(); + .map_err(|_| CredentialResponseError)?; let credential = match credential_response.credential { CredentialResponseType::Immediate { credential, .. } => credential, - _ => panic!("Credential was not a jwt_vc_json."), + CredentialResponseType::Deferred { .. } => { + return Err(UnsupportedDeferredCredentialResponseError) + } }; vec![credential] } _batch => { - todo!() + return Err(BatchCredentialRequestError); } }; diff --git a/agent_holder/src/offer/error.rs b/agent_holder/src/offer/error.rs index 7c44918a..eabbfdd3 100644 --- a/agent_holder/src/offer/error.rs +++ b/agent_holder/src/offer/error.rs @@ -2,8 +2,34 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum OfferError { + #[error("The Credential Offer could not be retrieved from the `credential_offer_uri`")] + CredentialOfferByReferenceRetrievalError, + #[error("The Credential Issuer Metadata could not be retrieved")] + CredentialIssuerMetadataRetrievalError, #[error("The Credential Offer has already been accepted and cannot be rejected anymore")] CredentialOfferStatusNotPendingError, + #[error("The Credential Offer is missing")] + MissingCredentialOfferError, + #[error("The Authorization Server Metadata could not be retrieved")] + AuthorizationServerMetadataRetrievalError, + #[error("The pre-authorized code is missing from the Credential Offer")] + MissingPreAuthorizedCodeError, + #[error("The Authorization Server Metadata is missing the `token_endpoint` parameter")] + MissingTokenEndpointError, + #[error("An error occurred while requesting the access token")] + TokenResponseError, #[error("The Credential Offer has not been accepted yet")] CredentialOfferStatusNotAcceptedError, + #[error("The Token Response is missing from the Credential Offer")] + MissingTokenResponseError, + #[error("The Credential Configurations are missing from the Credential Offer")] + MissingCredentialConfigurationsError, + #[error("The Credential Configuration is missing from the Credential Configurations")] + MissingCredentialConfigurationError, + #[error("An error occurred while requesting the credentials")] + CredentialResponseError, + #[error("Deferred Credential Responses are not supported")] + UnsupportedDeferredCredentialResponseError, + #[error("Batch Credential Request are not supported")] + BatchCredentialRequestError, } From 29e90d1c152827ecc5c97c84b82e8c43fede5dee Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 30 Aug 2024 16:03:44 +0200 Subject: [PATCH 25/31] fix: add error handling for to Offer aggregate --- agent_issuance/src/offer/aggregate.rs | 19 ++++++++++--------- agent_issuance/src/offer/error.rs | 4 ++++ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/agent_issuance/src/offer/aggregate.rs b/agent_issuance/src/offer/aggregate.rs index ad1bddd1..3229b808 100644 --- a/agent_issuance/src/offer/aggregate.rs +++ b/agent_issuance/src/offer/aggregate.rs @@ -93,23 +93,24 @@ impl Aggregate for Offer { }]), CreateFormUrlEncodedCredentialOffer { offer_id } => Ok(vec![FormUrlEncodedCredentialOfferCreated { offer_id, - form_url_encoded_credential_offer: self.credential_offer.as_ref().unwrap().to_string(), + form_url_encoded_credential_offer: self + .credential_offer + .as_ref() + .ok_or(MissingCredentialOfferError)? + .to_string(), }]), SendCredentialOffer { offer_id, target_url } => { + // TODO: add to `service`? let client = reqwest::Client::new(); - let response = client + client .get(target_url.clone()) - .json(self.credential_offer.as_ref().unwrap()) + .json(self.credential_offer.as_ref().ok_or(MissingCredentialOfferError)?) .send() .await - .unwrap(); + .map_err(|e| SendCredentialOfferError(e.to_string()))?; - if response.status().is_success() { - Ok(vec![CredentialOfferSent { offer_id, target_url }]) - } else { - todo!() - } + Ok(vec![CredentialOfferSent { offer_id, target_url }]) } CreateTokenResponse { offer_id, diff --git a/agent_issuance/src/offer/error.rs b/agent_issuance/src/offer/error.rs index 3cd038e7..bdbd4528 100644 --- a/agent_issuance/src/offer/error.rs +++ b/agent_issuance/src/offer/error.rs @@ -2,6 +2,10 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum OfferError { + #[error("Credential Offer is missing")] + MissingCredentialOfferError, + #[error("Something went wrong while trying to send the Credential Offer to the `target_url`: {0}")] + SendCredentialOfferError(String), #[error("Credential is missing")] MissingCredentialError, #[error("Missing `Proof` in Credential Request")] From cda0b762ba8bc349175d41a6812f4deb948d8d3c Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 30 Aug 2024 16:06:29 +0200 Subject: [PATCH 26/31] refactor: apply clippy suggestion --- agent_shared/src/config.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agent_shared/src/config.rs b/agent_shared/src/config.rs index 8e631f60..f4da6fd9 100644 --- a/agent_shared/src/config.rs +++ b/agent_shared/src/config.rs @@ -215,9 +215,9 @@ pub enum SupportedDidMethod { IotaRms, } -impl Into for SupportedDidMethod { - fn into(self) -> SubjectSyntaxType { - SubjectSyntaxType::try_from(self.to_string().as_str()).expect("convertion into `SubjectSyntaxType` failed") +impl From for SubjectSyntaxType { + fn from(val: SupportedDidMethod) -> Self { + SubjectSyntaxType::try_from(val.to_string().as_str()).expect("convertion into `SubjectSyntaxType` failed") } } From bb43a335ea86d11d56098c402e372dc539a667a5 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 30 Aug 2024 16:30:05 +0200 Subject: [PATCH 27/31] test: update Postman Collection --- .../postman/ssi-agent.postman_collection.json | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/agent_api_rest/postman/ssi-agent.postman_collection.json b/agent_api_rest/postman/ssi-agent.postman_collection.json index b0fbd6ba..924b87ad 100644 --- a/agent_api_rest/postman/ssi-agent.postman_collection.json +++ b/agent_api_rest/postman/ssi-agent.postman_collection.json @@ -70,6 +70,13 @@ "type": "text/javascript", "packages": {} } + }, + { + "listen": "prerequest", + "script": { + "exec": [], + "type": "text/javascript" + } } ], "request": { @@ -136,6 +143,13 @@ "type": "text/javascript", "packages": {} } + }, + { + "listen": "prerequest", + "script": { + "exec": [], + "type": "text/javascript" + } } ], "request": { @@ -162,6 +176,34 @@ } }, "response": [] + }, + { + "name": "offers_send", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"offerId\": \"{{OFFER_ID}}\",\n \"targetUrl\": \"{{HOST}}/openid4vci/offers\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{HOST}}/v0/offers/send", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "offers", + "send" + ] + } + }, + "response": [] } ] }, @@ -579,6 +621,118 @@ "response": [] } ] + }, + { + "name": "Holder", + "item": [ + { + "name": "offers", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const jsonData = JSON.parse(responseBody);", + "", + "if (jsonData && typeof jsonData === 'object') {", + " const receivedOfferId = Object.keys(jsonData)[0];", + "", + " if (receivedOfferId) {", + " pm.collectionVariables.set(\"RECEIVED_OFFER_ID\", receivedOfferId);", + " }", + "}" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/v0/holder/offers", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "holder", + "offers" + ] + } + }, + "response": [] + }, + { + "name": "credentials", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/v0/holder/credentials", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "holder", + "credentials" + ] + } + }, + "response": [] + }, + { + "name": "offers_accept", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{HOST}}/v0/holder/offers/{{RECEIVED_OFFER_ID}}/accept", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "holder", + "offers", + "{{RECEIVED_OFFER_ID}}", + "accept" + ] + } + }, + "response": [] + }, + { + "name": "offers_reject", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{HOST}}/v0/holder/offers/{{RECEIVED_OFFER_ID}}/reject", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "holder", + "offers", + "{{RECEIVED_OFFER_ID}}", + "reject" + ] + } + }, + "response": [] + } + ] } ], "event": [ @@ -641,6 +795,11 @@ "key": "REQUEST_URI", "value": "INITIAL_VALUE", "type": "string" + }, + { + "key": "RECEIVED_OFFER_ID", + "value": "INITIAL_VALUE", + "type": "string" } ] } \ No newline at end of file From 5aae168f2663b17854ba220db854f77327fb3020 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 30 Aug 2024 16:45:08 +0200 Subject: [PATCH 28/31] feat: add Events to `config.rs` --- agent_shared/src/config.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/agent_shared/src/config.rs b/agent_shared/src/config.rs index f4da6fd9..d5b49304 100644 --- a/agent_shared/src/config.rs +++ b/agent_shared/src/config.rs @@ -150,12 +150,16 @@ pub enum OfferEvent { #[derive(Debug, Serialize, Deserialize, Clone, strum::Display)] pub enum HolderCredentialEvent { - // FIX THIS + CredentialAdded, } #[derive(Debug, Serialize, Deserialize, Clone, strum::Display)] pub enum ReceivedOfferEvent { - // FIX THIS + CredentialOfferReceived, + CredentialOfferAccepted, + TokenResponseReceived, + CredentialResponseReceived, + CredentialOfferRejected, } #[derive(Debug, Serialize, Deserialize, Clone, strum::Display)] From 3056a0940d31908ab888730bcb34cc7964d48d22 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 30 Aug 2024 16:46:33 +0200 Subject: [PATCH 29/31] docs: add new Holder events to `agent_event_publisher_http` documentation --- agent_event_publisher_http/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/agent_event_publisher_http/README.md b/agent_event_publisher_http/README.md index 028d2308..2fa08dbf 100644 --- a/agent_event_publisher_http/README.md +++ b/agent_event_publisher_http/README.md @@ -47,6 +47,22 @@ ServerMetadataLoaded CredentialConfigurationAdded ``` +#### `holder_credential` + +``` +CredentialAdded +``` + +#### `received_offer` + +``` +CredentialOfferReceived +CredentialOfferAccepted +TokenResponseReceived +CredentialResponseReceived +CredentialOfferRejected +``` + #### `authorization_request` ``` From 266a1ba1e9fbd0efa91f52de7628e53d09cc9ca7 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 30 Aug 2024 18:03:07 +0200 Subject: [PATCH 30/31] docs: update JIT Credential Issuance documentation --- agent_application/docker/README.md | 27 ------------- agent_event_publisher_http/README.md | 57 ++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 27 deletions(-) diff --git a/agent_application/docker/README.md b/agent_application/docker/README.md index e25559f2..6f493e0a 100644 --- a/agent_application/docker/README.md +++ b/agent_application/docker/README.md @@ -49,30 +49,3 @@ variables: UNICORE__SECRET_MANAGER__ISSUER_DID: UNICORE__SECRET_MANAGER__ISSUER_FRAGMENT: ``` - -## Leveraging Just-in-Time Data Request Events - -UniCore facilitates dynamic integration with external systems through just-in-time data request events, dispatched seamlessly via an HTTP Event Publisher. This enables real-time data retrieval and on-demand generation, enhancing flexibility and efficiency in your SSI ecosystem. - -### Example Scenarios - -**Custom Credential Signing** - -UniCore facilitates the utilization of just-in-time data request events for customized credential signing workflows. This approach enables users to manage the signing process independently, offering greater control over credential issuance. When UniCore verifies a Credential Request from a Wallet, it triggers the `CredentialRequestVerified` event. By utilizing the HTTP Event Publisher, this event, containing essential identifiers like `offer_id` and `subject_id`, can be dispatched to external systems. Subsequently, external systems leverage these identifiers to generate and sign credentials, which are then submitted to UniCore's `/v0/credentials` endpoint. - -To integrate just-in-time data request events into your workflow, adhere to the following steps: - -1. Configure the HTTP Event Publisher to listen for the `CredentialRequestVerified` event. Refer to the [HTTP Event Publisher documentation](../../agent_event_publisher_http/README.md) for detailed configuration instructions: - - ```yaml - target_url: &target_url "https://my-domain.example.org/ssi-event-subscriber" - - offer: - { target_url: *target_url, target_events: [CredentialRequestVerified] } - ``` - -2. Upon initiation of the OpenID4VCI flow by a Wallet, the CredentialRequestVerified event is triggered, containing relevant identifiers. -3. The HTTP Event Publisher dispatches the event to the external system. Leveraging the provided identifiers, the external system generates and signs the credential, then submits it to UniCore's `/v0/credentials` endpoint. Refer to the [API specification](../../agent_api_rest/README.md)) for additional details on endpoint usage. - -By default, UniCore will wait up to 1000 ms for the signed credential to arrive. This parameter can be changed by -setting the `AGENT_API_REST_EXTERNAL_SERVER_RESPONSE_TIMEOUT_MS` environment variable. diff --git a/agent_event_publisher_http/README.md b/agent_event_publisher_http/README.md index 2fa08dbf..52465e79 100644 --- a/agent_event_publisher_http/README.md +++ b/agent_event_publisher_http/README.md @@ -77,3 +77,60 @@ AuthorizationRequestObjectSigned SIOPv2AuthorizationResponseVerified OID4VPAuthorizationResponseVerified ``` + +## Leveraging Just-in-Time Data Request Events + +UniCore facilitates dynamic integration with external systems through just-in-time data request events, dispatched seamlessly via the HTTP Event Publisher. This enables real-time data retrieval and on-demand generation, enhancing flexibility and efficiency in your SSI ecosystem. + +### Example Scenarios + +**Custom Credential Signing** + +UniCore facilitates the utilization of just-in-time data request events for customized credential signing workflows. This approach enables users to manage the signing process independently, offering greater control over credential issuance. When UniCore verifies a Credential Request from a Wallet, it triggers the `CredentialRequestVerified` event. By utilizing the HTTP Event Publisher, this event, containing essential identifiers like `offer_id` and `subject_id`, can be dispatched to external systems. Subsequently, external systems leverage these identifiers to generate and sign credentials, which are then submitted to UniCore's `/v0/credentials` endpoint. + +To integrate just-in-time data request events into your workflow, adhere to the following steps: + +1. Configure the HTTP Event Publisher to listen for the `CredentialRequestVerified` event. The following configuration + can be added to your `config.yaml` file: + ```yaml + event_publishers: + http: + enabled: true + target_url: "https://your-server.org/event-subscriber" + events: + offer: [CredentialRequestVerified] + ``` +2. The above configuration makes sure that whenever a Wallet sends a Credential Request, the HTTP Event Publisher will + dispatch the `CredentialRequestVerified` event to the specified URL once it successfully verified the Credential + Request, e.g: + ```json + POST /event-subscriber HTTP/1.1 + Host: https://your-server.org + Content-Type: application/json + Content-Length: 328 + { + "CredentialRequestVerified": { + "offer_id": "001", + "subject_id": "did:jwk:eyJhbGciOiJFUzI1NiIsImNydiI6IlAtMjU2Iiwia2lkIjoieERDQVBRbHRVa2JZMnByTkdpT0ItNWJ2T0pnZnQ0NVJqYjM2RWNjSWNGdyIsImt0eSI6IkVDIiwieCI6Im02b3EySFF6NmluSk8xbzg1VUM5VVEyamxJRFJld0ROVS0ybUktVThKN1UiLCJ5Ijoia0NwbTcwbXpCT3Y0OWFPdHdmRUdxVW1fSkllWXlZeWdWSXpKaFpXY1ZnTSJ9" + } + } + ``` +3. Now your system can apply its own logic and create and sign a Credential based on the data received from the Event. + The signed Credential can then be submitted to UniCore's `/v0/credentials` endpoint, e.g: + ```json + POST /v0/credentials HTTP/1.1 + Host: https://unicore-server.org + Content-Type: application/json + Content-Length: 328 + { + "offerId": "001", + "credential": "", + "isSigned": true, + "credentialConfigurationId": "" + } + ``` +4. Once UniCore receives the signed Credential, it will finalize the issuance process by embedding the signed Credential + into the Credential Response to the Wallet. + +By default, UniCore will wait up to 1000 ms for the signed credential to arrive. This parameter can be changed by +setting the `AGENT_API_REST_EXTERNAL_SERVER_RESPONSE_TIMEOUT_MS` environment variable. From 51204043c464f2867708495700fb4caf9a700856 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 30 Aug 2024 18:06:04 +0200 Subject: [PATCH 31/31] docs: use http code block --- agent_event_publisher_http/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agent_event_publisher_http/README.md b/agent_event_publisher_http/README.md index 52465e79..82de1986 100644 --- a/agent_event_publisher_http/README.md +++ b/agent_event_publisher_http/README.md @@ -103,7 +103,7 @@ To integrate just-in-time data request events into your workflow, adhere to the 2. The above configuration makes sure that whenever a Wallet sends a Credential Request, the HTTP Event Publisher will dispatch the `CredentialRequestVerified` event to the specified URL once it successfully verified the Credential Request, e.g: - ```json + ```http POST /event-subscriber HTTP/1.1 Host: https://your-server.org Content-Type: application/json @@ -117,7 +117,7 @@ To integrate just-in-time data request events into your workflow, adhere to the ``` 3. Now your system can apply its own logic and create and sign a Credential based on the data received from the Event. The signed Credential can then be submitted to UniCore's `/v0/credentials` endpoint, e.g: - ```json + ```http POST /v0/credentials HTTP/1.1 Host: https://unicore-server.org Content-Type: application/json