From f61e9b539e48d16f9b49c05f6cd96d778352cd51 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Wed, 26 Jun 2024 11:35:26 +0200 Subject: [PATCH 01/68] WIP --- agent_application/src/main.rs | 64 ++++++++++++++++++- agent_shared/src/lib.rs | 1 + .../src/linked_verifiable_presentation.rs | 43 +++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 agent_shared/src/linked_verifiable_presentation.rs diff --git a/agent_application/src/main.rs b/agent_application/src/main.rs index d9cfceab..7c1f9ad5 100644 --- a/agent_application/src/main.rs +++ b/agent_application/src/main.rs @@ -5,11 +5,12 @@ use agent_secret_manager::{secret_manager, subject::Subject}; use agent_shared::{ config, domain_linkage::create_did_configuration_resource, + linked_verifiable_presentation::create_linked_verifiable_presentation_resource, metadata::{load_metadata, Metadata}, }; use agent_store::{in_memory, postgres, EventPublisher}; use agent_verification::services::VerificationServices; -use axum::{routing::get, Json}; +use axum::{http, response::IntoResponse, routing::get, Json}; use identity_document::service::{Service, ServiceEndpoint}; use std::sync::Arc; use tokio::{fs, io}; @@ -95,6 +96,7 @@ async fn main() -> io::Result<()> { } else { None }; + // Domain Linkage let did_configuration_resource = if config!("domain_linkage_enabled", bool).unwrap_or(false) { Some( @@ -112,6 +114,27 @@ async fn main() -> io::Result<()> { None }; + // Linked Verifiable Presentation + let linked_verifiable_presentation_resource = + if config!("linked_verifiable_presentation_enabled", bool).unwrap_or(false) { + Some( + create_linked_verifiable_presentation_resource( + url.clone(), + config!("linked_verifiable_credential", String) + .expect("`AGENT_CONFIG_LINKED_VERIFIABLE_CREDENTIAL` is not set") + .into(), + did_document + .clone() + .expect("No DID document found to create a Linked Verifiable Presentation Resource for"), + secret_manager().await, + ) + .await + .expect("Failed to create DID Configuration Resource"), + ) + } else { + None + }; + if let Some(mut did_document) = did_document { if let Some(did_configuration_resource) = did_configuration_resource { // Create a new service and add it to the DID document. @@ -136,6 +159,45 @@ async fn main() -> io::Result<()> { info!("Serving DID Configuration (Domain Linkage) at `{path}`"); app = app.route(path, get(Json(did_configuration_resource))); } + + if let Some(linked_verifiable_presentation_resource) = linked_verifiable_presentation_resource { + // Create a new service and add it to the DID document. + + // FIX THISS + let path = "/linked-verifiable-presentation.jwt"; + + let service = Service::builder(Default::default()) + .id(format!("{}#service-2", did_document.id()).parse().unwrap()) + .type_("LinkedVerifiablePresentation") + .service_endpoint( + serde_json::from_value::(serde_json::json!( + { + "origins": [format!("{}{path}", url.origin().ascii_serialization())] + } + )) + .unwrap(), + ) + .build() + .expect("Failed to create Linked Verifiable Presentation Resource"); + did_document + .insert_service(service) + .expect("Service already exists in DID Document"); + + let linked_verifiable_presentation = linked_verifiable_presentation_resource.as_str().to_string(); + + info!("Serving Linked Verifiable Presentation at `{path}`"); + app = app.route( + path, + get(|| async { + ( + [(http::header::CONTENT_TYPE, "application/jwt")], + linked_verifiable_presentation, + ) + .into_response() + }), + ); + } + let path = "/.well-known/did.json"; info!("Serving `did:web` document at `{path}`"); app = app.route(path, get(Json(did_document))); diff --git a/agent_shared/src/lib.rs b/agent_shared/src/lib.rs index 054ab71e..d89e9cf1 100644 --- a/agent_shared/src/lib.rs +++ b/agent_shared/src/lib.rs @@ -5,6 +5,7 @@ pub mod error; pub mod generic_query; pub mod handlers; pub mod issuance; +pub mod linked_verifiable_presentation; pub mod metadata; pub mod url_utils; diff --git a/agent_shared/src/linked_verifiable_presentation.rs b/agent_shared/src/linked_verifiable_presentation.rs new file mode 100644 index 00000000..1fad1c22 --- /dev/null +++ b/agent_shared/src/linked_verifiable_presentation.rs @@ -0,0 +1,43 @@ +use crate::error::SharedError; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use did_manager::SecretManager; +use identity_credential::{credential::Jwt, presentation::Presentation}; +use identity_document::document::CoreDocument; +use jsonwebtoken::{Algorithm, Header}; + +pub async fn create_linked_verifiable_presentation_resource( + url: url::Url, + verifiable_credential_jwt: Jwt, + did_document: CoreDocument, + secret_manager: SecretManager, +) -> Result { + let presentation = Presentation::builder(url.into(), identity_core::common::Object::new()) + .credential(Jwt::from(verifiable_credential_jwt)) + .build() + .map_err(|e| SharedError::Generic(e.to_string()))?; + + let payload = presentation.serialize_jwt(&Default::default()).expect("FIX THISS"); + + // TODO: make distinction between different DID methods. + let subject_did = did_document.id().to_string(); + + // Compose JWT + let header = Header { + alg: Algorithm::EdDSA, + typ: Some("JWT".to_string()), + kid: Some(format!("{subject_did}#key-0")), + ..Default::default() + }; + + let message = [ + URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap().as_slice()), + URL_SAFE_NO_PAD.encode(payload.as_bytes()), + ] + .join("."); + + let proof_value = secret_manager.sign(message.as_bytes()).await.unwrap(); + let signature = URL_SAFE_NO_PAD.encode(proof_value.as_slice()); + let message = [message, signature].join("."); + + Ok(Jwt::from(message)) +} From 4ffa6d10cfcd1f54cd59035bb9d441f77f154678 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Thu, 22 Aug 2024 10:28:16 +0200 Subject: [PATCH 02/68] 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 03/68] 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 04/68] 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 05/68] 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 06/68] 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 07/68] 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 08/68] 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 09/68] 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 10/68] 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 11/68] 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 12/68] 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 13/68] 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 14/68] 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 15/68] 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 16/68] 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 17/68] 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 18/68] 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 19/68] 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 20/68] 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 21/68] 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 22/68] 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 23/68] 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 24/68] 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 25/68] 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 26/68] 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 27/68] 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 28/68] 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 29/68] 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 30/68] 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 ecf05c4728bf12142dba3a2802c88d3116cd2d87 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Wed, 18 Sep 2024 12:49:31 +0200 Subject: [PATCH 31/68] feat: init `agent_identity` --- Cargo.lock | 94 +++++--- Cargo.toml | 13 +- agent_api_rest/Cargo.toml | 4 + agent_api_rest/src/holder/holder/mod.rs | 1 + .../src/holder/holder/presentations/mod.rs | 73 +++++++ .../presentations/presentation_signed.rs | 27 +++ agent_api_rest/src/holder/mod.rs | 6 + agent_api_rest/src/identity/mod.rs | 23 ++ agent_api_rest/src/identity/services/mod.rs | 37 ++++ agent_api_rest/src/identity/well_known/did.rs | 23 ++ .../identity/well_known/did_configuration.rs | 25 +++ agent_api_rest/src/identity/well_known/mod.rs | 2 + agent_api_rest/src/lib.rs | 5 + agent_application/Cargo.toml | 3 +- agent_application/src/main.rs | 184 +++------------- agent_holder/Cargo.toml | 3 + agent_holder/src/lib.rs | 1 + agent_holder/src/presentation/README.md | 3 + agent_holder/src/presentation/aggregate.rs | 117 ++++++++++ agent_holder/src/presentation/command.rs | 11 + agent_holder/src/presentation/error.rs | 4 + agent_holder/src/presentation/event.rs | 26 +++ agent_holder/src/presentation/mod.rs | 5 + .../presentation/views/all_presentations.rs | 23 ++ agent_holder/src/presentation/views/mod.rs | 22 ++ agent_holder/src/state.rs | 14 +- agent_identity/Cargo.toml | 29 +++ agent_identity/src/document/README.md | 3 + agent_identity/src/document/aggregate.rs | 89 ++++++++ agent_identity/src/document/command.rs | 10 + agent_identity/src/document/error.rs | 4 + agent_identity/src/document/event.rs | 25 +++ agent_identity/src/document/mod.rs | 5 + agent_identity/src/document/views/mod.rs | 18 ++ agent_identity/src/lib.rs | 6 + agent_identity/src/service/README.md | 3 + agent_identity/src/service/aggregate.rs | 200 ++++++++++++++++++ agent_identity/src/service/command.rs | 13 ++ agent_identity/src/service/error.rs | 4 + agent_identity/src/service/event.rs | 37 ++++ agent_identity/src/service/mod.rs | 5 + agent_identity/src/service/views/mod.rs | 25 +++ agent_identity/src/services.rs | 13 ++ agent_identity/src/state.rs | 107 ++++++++++ agent_issuance/src/credential/aggregate.rs | 20 +- agent_secret_manager/src/subject.rs | 2 +- agent_shared/src/config.rs | 2 +- agent_shared/src/domain_linkage/mod.rs | 123 ----------- .../verifiable_credential_jwt.rs | 82 ------- agent_shared/src/lib.rs | 2 - .../src/linked_verifiable_presentation.rs | 43 ---- agent_store/Cargo.toml | 1 + agent_store/src/in_memory.rs | 86 +++++++- agent_store/src/lib.rs | 82 ++++--- agent_store/src/postgres.rs | 99 +++++++-- .../src/authorization_request/aggregate.rs | 4 +- 56 files changed, 1384 insertions(+), 507 deletions(-) create mode 100644 agent_api_rest/src/holder/holder/presentations/mod.rs create mode 100644 agent_api_rest/src/holder/holder/presentations/presentation_signed.rs create mode 100644 agent_api_rest/src/identity/mod.rs create mode 100644 agent_api_rest/src/identity/services/mod.rs create mode 100644 agent_api_rest/src/identity/well_known/did.rs create mode 100644 agent_api_rest/src/identity/well_known/did_configuration.rs create mode 100644 agent_api_rest/src/identity/well_known/mod.rs create mode 100644 agent_holder/src/presentation/README.md create mode 100644 agent_holder/src/presentation/aggregate.rs create mode 100644 agent_holder/src/presentation/command.rs create mode 100644 agent_holder/src/presentation/error.rs create mode 100644 agent_holder/src/presentation/event.rs create mode 100644 agent_holder/src/presentation/mod.rs create mode 100644 agent_holder/src/presentation/views/all_presentations.rs create mode 100644 agent_holder/src/presentation/views/mod.rs create mode 100644 agent_identity/Cargo.toml create mode 100644 agent_identity/src/document/README.md create mode 100644 agent_identity/src/document/aggregate.rs create mode 100644 agent_identity/src/document/command.rs create mode 100644 agent_identity/src/document/error.rs create mode 100644 agent_identity/src/document/event.rs create mode 100644 agent_identity/src/document/mod.rs create mode 100644 agent_identity/src/document/views/mod.rs create mode 100644 agent_identity/src/lib.rs create mode 100644 agent_identity/src/service/README.md create mode 100644 agent_identity/src/service/aggregate.rs create mode 100644 agent_identity/src/service/command.rs create mode 100644 agent_identity/src/service/error.rs create mode 100644 agent_identity/src/service/event.rs create mode 100644 agent_identity/src/service/mod.rs create mode 100644 agent_identity/src/service/views/mod.rs create mode 100644 agent_identity/src/services.rs create mode 100644 agent_identity/src/state.rs delete mode 100644 agent_shared/src/domain_linkage/mod.rs delete mode 100644 agent_shared/src/domain_linkage/verifiable_credential_jwt.rs delete mode 100644 agent_shared/src/linked_verifiable_presentation.rs diff --git a/Cargo.lock b/Cargo.lock index a4ff67e2..bb1a9d60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,7 @@ version = "0.1.0" dependencies = [ "agent_event_publisher_http", "agent_holder", + "agent_identity", "agent_issuance", "agent_secret_manager", "agent_shared", @@ -72,9 +73,11 @@ dependencies = [ "axum 0.7.5", "axum-auth 0.7.0", "axum-macros", + "did_manager", "futures", "http-api-problem", "hyper 1.4.1", + "identity_credential", "jsonwebtoken", "lazy_static", "mime", @@ -107,6 +110,7 @@ dependencies = [ "agent_api_rest", "agent_event_publisher_http", "agent_holder", + "agent_identity", "agent_issuance", "agent_secret_manager", "agent_shared", @@ -114,7 +118,7 @@ dependencies = [ "agent_verification", "axum 0.7.5", "did_manager", - "identity_document", + "identity_credential", "identity_verification", "serde_json", "tokio", @@ -161,8 +165,11 @@ dependencies = [ "async-std", "async-trait", "axum 0.7.5", + "base64 0.22.1", "cqrs-es", "did_manager", + "identity_credential", + "identity_verification", "jsonwebtoken", "lazy_static", "mime", @@ -182,6 +189,30 @@ dependencies = [ "tracing-test", ] +[[package]] +name = "agent_identity" +version = "0.1.0" +dependencies = [ + "agent_secret_manager", + "agent_shared", + "async-trait", + "base64 0.22.1", + "cqrs-es", + "derivative", + "did_manager", + "identity_core", + "identity_credential", + "identity_document", + "identity_verification", + "jsonwebtoken", + "oid4vc-core", + "oid4vci", + "serde", + "serde_json", + "thiserror", + "tracing", +] + [[package]] name = "agent_issuance" version = "0.1.0" @@ -284,6 +315,7 @@ name = "agent_store" version = "0.1.0" dependencies = [ "agent_holder", + "agent_identity", "agent_issuance", "agent_shared", "agent_verification", @@ -1408,7 +1440,7 @@ checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" [[package]] name = "consumer" version = "0.1.0" -source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.2#6880b7f81b23e6cf1bbdc76d1dea8d2c1f6ef559" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "did_iota", "did_jwk", @@ -1974,7 +2006,7 @@ dependencies = [ [[package]] name = "did_iota" version = "0.1.0" -source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.2#6880b7f81b23e6cf1bbdc76d1dea8d2c1f6ef559" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "bls12_381_plus 0.8.15", "identity_iota", @@ -1988,7 +2020,7 @@ dependencies = [ [[package]] name = "did_jwk" version = "0.1.0" -source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.2#6880b7f81b23e6cf1bbdc76d1dea8d2c1f6ef559" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "did-jwk", "identity_iota", @@ -2005,7 +2037,7 @@ dependencies = [ [[package]] name = "did_key" version = "0.1.0" -source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.2#6880b7f81b23e6cf1bbdc76d1dea8d2c1f6ef559" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "did-method-key", "identity_iota", @@ -2023,7 +2055,7 @@ dependencies = [ [[package]] name = "did_manager" version = "0.1.0" -source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.2#6880b7f81b23e6cf1bbdc76d1dea8d2c1f6ef559" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "consumer", "producer", @@ -2051,7 +2083,7 @@ dependencies = [ [[package]] name = "did_web" version = "0.1.0" -source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.2#6880b7f81b23e6cf1bbdc76d1dea8d2c1f6ef559" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "did-web", "identity_iota", @@ -2070,7 +2102,7 @@ dependencies = [ [[package]] name = "dif-presentation-exchange" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=23facd4#23facd4946e4b2e54ef87f42746c68ad952ae205" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=12fed14#12fed1411ff3c0e1797090f386e44694f7a279b8" dependencies = [ "getset", "jsonpath_lib", @@ -2849,7 +2881,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.2.6", + "indexmap 2.5.0", "slab", "tokio", "tokio-util", @@ -2868,7 +2900,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.1.0", - "indexmap 2.2.6", + "indexmap 2.5.0", "slab", "tokio", "tokio-util", @@ -3292,7 +3324,7 @@ dependencies = [ "identity_did", "identity_document", "identity_verification", - "indexmap 2.2.6", + "indexmap 2.5.0", "itertools 0.11.0", "once_cell", "roaring", @@ -3329,7 +3361,7 @@ dependencies = [ "identity_core", "identity_did", "identity_verification", - "indexmap 2.2.6", + "indexmap 2.5.0", "serde", "strum 0.25.0", "thiserror", @@ -3454,7 +3486,7 @@ dependencies = [ [[package]] name = "identity_stronghold_ext" version = "0.1.0" -source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.2#6880b7f81b23e6cf1bbdc76d1dea8d2c1f6ef559" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "async-trait", "elliptic-curve 0.13.8", @@ -3537,9 +3569,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -3952,7 +3984,7 @@ checksum = "179551c27c512c948af1edaf4bd7e1d1486d223f8ec4fd41cd760f7645fd4197" dependencies = [ "cargo-license", "data-encoding", - "indexmap 2.2.6", + "indexmap 2.5.0", "json-unflattening", "serde", "serde_json", @@ -4727,7 +4759,7 @@ dependencies = [ [[package]] name = "oid4vc-core" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=23facd4#23facd4946e4b2e54ef87f42746c68ad952ae205" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=12fed14#12fed1411ff3c0e1797090f386e44694f7a279b8" dependencies = [ "anyhow", "async-trait", @@ -4751,7 +4783,7 @@ dependencies = [ [[package]] name = "oid4vc-manager" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=23facd4#23facd4946e4b2e54ef87f42746c68ad952ae205" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=12fed14#12fed1411ff3c0e1797090f386e44694f7a279b8" dependencies = [ "anyhow", "async-trait", @@ -4783,7 +4815,7 @@ dependencies = [ [[package]] name = "oid4vci" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=23facd4#23facd4946e4b2e54ef87f42746c68ad952ae205" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=12fed14#12fed1411ff3c0e1797090f386e44694f7a279b8" dependencies = [ "anyhow", "derivative", @@ -4806,7 +4838,7 @@ dependencies = [ [[package]] name = "oid4vp" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=23facd4#23facd4946e4b2e54ef87f42746c68ad952ae205" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=12fed14#12fed1411ff3c0e1797090f386e44694f7a279b8" dependencies = [ "anyhow", "chrono", @@ -5431,7 +5463,7 @@ dependencies = [ [[package]] name = "producer" version = "0.1.0" -source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.2#6880b7f81b23e6cf1bbdc76d1dea8d2c1f6ef559" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "did_iota", "did_jwk", @@ -6372,7 +6404,7 @@ version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.5.0", "itoa", "ryu", "serde", @@ -6456,7 +6488,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.6", + "indexmap 2.5.0", "serde", "serde_derive", "serde_json", @@ -6494,7 +6526,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.5.0", "itoa", "ryu", "serde", @@ -6593,7 +6625,7 @@ dependencies = [ [[package]] name = "shared" version = "0.1.0" -source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.2#6880b7f81b23e6cf1bbdc76d1dea8d2c1f6ef559" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "identity_iota", "identity_storage", @@ -6669,7 +6701,7 @@ dependencies = [ [[package]] name = "siopv2" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=23facd4#23facd4946e4b2e54ef87f42746c68ad952ae205" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=12fed14#12fed1411ff3c0e1797090f386e44694f7a279b8" dependencies = [ "anyhow", "async-trait", @@ -6827,7 +6859,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.2.6", + "indexmap 2.5.0", "log", "memchr", "once_cell", @@ -7703,7 +7735,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.5.0", "toml_datetime", "winnow 0.5.40", ] @@ -7714,7 +7746,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.5.0", "toml_datetime", "winnow 0.5.40", ] @@ -7725,7 +7757,7 @@ version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.5.0", "serde", "serde_spanned", "toml_datetime", @@ -8687,7 +8719,7 @@ dependencies = [ "crossbeam-utils", "displaydoc", "flate2", - "indexmap 2.2.6", + "indexmap 2.5.0", "memchr", "thiserror", "zopfli", diff --git a/Cargo.toml b/Cargo.toml index c913ede1..318a16b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "agent_application", "agent_event_publisher_http", "agent_holder", + "agent_identity", "agent_issuance", "agent_secret_manager", "agent_shared", @@ -18,12 +19,12 @@ edition = "2021" rust-version = "1.76.0" [workspace.dependencies] -did_manager = { git = "https://git@github.com/impierce/did-manager.git", tag = "v1.0.0-beta.2" } -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" } +did_manager = { git = "https://git@github.com/impierce/did-manager.git", tag = "v1.0.0-beta.3" } +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" } async-trait = "0.1" axum = { version = "0.7", features = ["tracing"] } diff --git a/agent_api_rest/Cargo.toml b/agent_api_rest/Cargo.toml index 73997c65..fb3bfabc 100644 --- a/agent_api_rest/Cargo.toml +++ b/agent_api_rest/Cargo.toml @@ -6,6 +6,7 @@ rust-version.workspace = true [dependencies] agent_holder = { path = "../agent_holder" } +agent_identity = { path = "../agent_identity" } agent_issuance = { path = "../agent_issuance" } agent_shared = { path = "../agent_shared" } agent_verification = { path = "../agent_verification" } @@ -13,8 +14,10 @@ agent_verification = { path = "../agent_verification" } axum.workspace = true axum-auth = "0.7" axum-macros = "0.4" +did_manager.workspace = true http-api-problem = "0.57" hyper = { version = "1.2" } +identity_credential.workspace = true oid4vc-core.workspace = true oid4vci.workspace = true oid4vp.workspace = true @@ -31,6 +34,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_identity = { path = "../agent_identity", 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"] } diff --git a/agent_api_rest/src/holder/holder/mod.rs b/agent_api_rest/src/holder/holder/mod.rs index 1a09baa0..1992a65b 100644 --- a/agent_api_rest/src/holder/holder/mod.rs +++ b/agent_api_rest/src/holder/holder/mod.rs @@ -1,2 +1,3 @@ pub mod credentials; pub mod offers; +pub mod presentations; diff --git a/agent_api_rest/src/holder/holder/presentations/mod.rs b/agent_api_rest/src/holder/holder/presentations/mod.rs new file mode 100644 index 00000000..5fea998d --- /dev/null +++ b/agent_api_rest/src/holder/holder/presentations/mod.rs @@ -0,0 +1,73 @@ +pub mod presentation_signed; + +use agent_holder::{ + credential::queries::CredentialView, presentation::command::PresentationCommand, state::HolderState, +}; +use agent_shared::handlers::{command_handler, query_handler}; +use axum::{ + extract::State, + response::{IntoResponse, Response}, + Json, +}; +use hyper::StatusCode; +use identity_credential::credential::Jwt; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use tracing::info; + +#[axum_macros::debug_handler] +pub(crate) async fn get_presentations(State(state): State) -> Response { + match query_handler("all_presentations", &state.query.all_presentations).await { + Ok(Some(all_presentations_view)) => (StatusCode::OK, Json(all_presentations_view)).into_response(), + Ok(None) => (StatusCode::OK, Json(json!({}))).into_response(), + _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } +} + +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PresentationsEndpointRequest { + pub credential_ids: Vec, +} + +#[axum_macros::debug_handler] +pub(crate) async fn post_presentations(State(state): State, Json(payload): Json) -> Response { + info!("Request Body: {}", payload); + + let Ok(PresentationsEndpointRequest { credential_ids }) = serde_json::from_value(payload) else { + return (StatusCode::BAD_REQUEST, "invalid payload").into_response(); + }; + + let mut credentials = vec![]; + + for credential_id in credential_ids { + match query_handler(&credential_id, &state.query.credential).await { + Ok(Some(CredentialView { + credential: Some(credential), + .. + })) => { + let credential = Jwt::from(credential.as_str().unwrap().to_string()); + credentials.push(credential); + } + Ok(None) => return StatusCode::NOT_FOUND.into_response(), + _ => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } + } + + let presentation_id = uuid::Uuid::new_v4().to_string(); + + let command = PresentationCommand::CreatePresentation { + presentation_id: presentation_id.clone(), + signed_credentials: credentials, + }; + + command_handler(&presentation_id, &state.command.presentation, command) + .await + .unwrap(); + + match query_handler(&presentation_id, &state.query.presentation).await { + Ok(Some(presentation_view)) => (StatusCode::OK, Json(presentation_view)).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/presentations/presentation_signed.rs b/agent_api_rest/src/holder/holder/presentations/presentation_signed.rs new file mode 100644 index 00000000..7a597ced --- /dev/null +++ b/agent_api_rest/src/holder/holder/presentations/presentation_signed.rs @@ -0,0 +1,27 @@ +use agent_holder::{presentation::aggregate::Presentation, state::HolderState}; +use agent_shared::handlers::query_handler; +use axum::{ + extract::{Path, State}, + response::{IntoResponse, Response}, +}; +use hyper::{header, StatusCode}; + +#[axum_macros::debug_handler] +pub(crate) async fn presentation_signed( + State(state): State, + Path(presentation_id): Path, +) -> Response { + match query_handler(&presentation_id, &state.query.presentation).await { + Ok(Some(Presentation { + signed_presentation: Some(signed_presentation), + .. + })) => ( + StatusCode::OK, + [(header::CONTENT_TYPE, "application/jwt")], + signed_presentation.as_str().to_string(), + ) + .into_response(), + Ok(None) => (StatusCode::NOT_FOUND).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 7ea56ad7..0cb17c93 100644 --- a/agent_api_rest/src/holder/mod.rs +++ b/agent_api_rest/src/holder/mod.rs @@ -11,6 +11,7 @@ use crate::API_VERSION; use agent_holder::state::HolderState; use axum::routing::get; use axum::{routing::post, Router}; +use holder::presentations::{get_presentations, post_presentations, presentation_signed::presentation_signed}; pub fn router(holder_state: HolderState) -> Router { Router::new() @@ -18,6 +19,11 @@ pub fn router(holder_state: HolderState) -> Router { API_VERSION, Router::new() .route("/holder/credentials", get(credentials)) + .route("/holder/presentations", get(get_presentations).post(post_presentations)) + .route( + "/holder/presentations/:presentation_id/signed", + get(presentation_signed), + ) .route("/holder/offers", get(offers)) .route("/holder/offers/:offer_id/accept", post(accept)) .route("/holder/offers/:offer_id/reject", post(reject)), diff --git a/agent_api_rest/src/identity/mod.rs b/agent_api_rest/src/identity/mod.rs new file mode 100644 index 00000000..3e5063c6 --- /dev/null +++ b/agent_api_rest/src/identity/mod.rs @@ -0,0 +1,23 @@ +pub mod services; +pub mod well_known; + +use agent_identity::state::IdentityState; +use axum::{ + routing::{get, post}, + Router, +}; +use services::linked_vp; +use well_known::{did::did, did_configuration::did_configuration}; + +use crate::API_VERSION; + +pub fn router(identity_state: IdentityState) -> Router { + Router::new() + .nest( + API_VERSION, + Router::new().route("/services/linked-vp/:presentation_id", post(linked_vp)), + ) + .route("/.well-known/did.json", get(did)) + .route("/.well-known/did-configuration.json", get(did_configuration)) + .with_state(identity_state) +} diff --git a/agent_api_rest/src/identity/services/mod.rs b/agent_api_rest/src/identity/services/mod.rs new file mode 100644 index 00000000..8f08603f --- /dev/null +++ b/agent_api_rest/src/identity/services/mod.rs @@ -0,0 +1,37 @@ +use agent_identity::{document::command::DocumentCommand, service::command::ServiceCommand, state::IdentityState}; +use agent_shared::handlers::{command_handler, query_handler}; +use axum::{ + extract::{Path, State}, + response::{IntoResponse, Response}, +}; +use did_manager::DidMethod; +use hyper::StatusCode; + +#[axum_macros::debug_handler] +pub(crate) async fn linked_vp(State(state): State, Path(presentation_id): Path) -> Response { + let service_id = "linked-verifiable-presentation-service".to_string(); + let command = ServiceCommand::CreateLinkedVerifiablePresentationService { + service_id: service_id.clone(), + presentation_id, + }; + + // Create a linked verifiable presentation service. + command_handler(&service_id, &state.command.service, command) + .await + .unwrap(); + + let service = query_handler(&service_id, &state.query.service) + .await + .unwrap() + .unwrap() + .service + .unwrap(); + + let command = DocumentCommand::AddService { service }; + + command_handler(&DidMethod::Web.to_string(), &state.command.document, command) + .await + .unwrap(); + + StatusCode::OK.into_response() +} diff --git a/agent_api_rest/src/identity/well_known/did.rs b/agent_api_rest/src/identity/well_known/did.rs new file mode 100644 index 00000000..c873a369 --- /dev/null +++ b/agent_api_rest/src/identity/well_known/did.rs @@ -0,0 +1,23 @@ +use agent_identity::{document::views::DocumentView, state::IdentityState}; +use agent_shared::handlers::query_handler; +use axum::{ + extract::State, + response::{IntoResponse, Response}, + Json, +}; +use did_manager::DidMethod; +use hyper::StatusCode; + +#[axum_macros::debug_handler] +pub(crate) async fn did(State(state): State) -> Response { + // TODO: check if enabled + // Get the DID Document if it exists. + match query_handler(&DidMethod::Web.to_string(), &state.query.document).await { + Ok(Some(DocumentView { + document: Some(document), + .. + })) => (StatusCode::OK, Json(document)).into_response(), + Ok(None) => StatusCode::NOT_FOUND.into_response(), + _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } +} diff --git a/agent_api_rest/src/identity/well_known/did_configuration.rs b/agent_api_rest/src/identity/well_known/did_configuration.rs new file mode 100644 index 00000000..6060ec6e --- /dev/null +++ b/agent_api_rest/src/identity/well_known/did_configuration.rs @@ -0,0 +1,25 @@ +use agent_identity::{ + service::{aggregate::ServiceResource, views::ServiceView}, + state::{IdentityState, DOMAIN_LINKAGE_SERVICE_ID}, +}; +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 did_configuration(State(state): State) -> Response { + // TODO: check if enabled + // Get the DomainLinkageConfiguration if it exists. + match query_handler(DOMAIN_LINKAGE_SERVICE_ID, &state.query.service).await { + Ok(Some(ServiceView { + resource: Some(ServiceResource::DomainLinkage(domain_linkage_configuration)), + .. + })) => (StatusCode::OK, Json(domain_linkage_configuration)).into_response(), + Ok(None) => StatusCode::NOT_FOUND.into_response(), + _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } +} diff --git a/agent_api_rest/src/identity/well_known/mod.rs b/agent_api_rest/src/identity/well_known/mod.rs new file mode 100644 index 00000000..225b8c40 --- /dev/null +++ b/agent_api_rest/src/identity/well_known/mod.rs @@ -0,0 +1,2 @@ +pub mod did; +pub mod did_configuration; diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index b282b706..6cd05f36 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -1,8 +1,10 @@ pub mod holder; +pub mod identity; pub mod issuance; pub mod verification; use agent_holder::state::HolderState; +use agent_identity::state::IdentityState; use agent_issuance::state::IssuanceState; use agent_shared::{config::config, ConfigError}; use agent_verification::state::VerificationState; @@ -14,6 +16,7 @@ pub const API_VERSION: &str = "/v0"; #[derive(Default)] pub struct ApplicationState { + pub identity_state: Option, pub issuance_state: Option, pub holder_state: Option, pub verification_state: Option, @@ -21,6 +24,7 @@ pub struct ApplicationState { pub fn app( ApplicationState { + identity_state, issuance_state, holder_state, verification_state, @@ -30,6 +34,7 @@ pub fn app( .nest( &get_base_path().unwrap_or_default(), Router::new() + .merge(identity_state.map(identity::router).unwrap_or_default()) .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()), diff --git a/agent_application/Cargo.toml b/agent_application/Cargo.toml index a919c707..246dfce8 100644 --- a/agent_application/Cargo.toml +++ b/agent_application/Cargo.toml @@ -8,6 +8,7 @@ rust-version.workspace = true agent_api_rest = { path = "../agent_api_rest" } agent_event_publisher_http = { path = "../agent_event_publisher_http" } agent_holder = { path = "../agent_holder" } +agent_identity = { path = "../agent_identity" } agent_issuance = { path = "../agent_issuance" } agent_secret_manager = { path = "../agent_secret_manager" } agent_shared = { path = "../agent_shared" } @@ -16,7 +17,7 @@ agent_verification = { path = "../agent_verification" } axum.workspace = true did_manager.workspace = true -identity_document = { version = "1.3" } +identity_credential.workspace = true identity_verification.workspace = true serde_json.workspace = true tokio.workspace = true diff --git a/agent_application/src/main.rs b/agent_application/src/main.rs index 10fc0cb9..2b01d32e 100644 --- a/agent_application/src/main.rs +++ b/agent_application/src/main.rs @@ -3,19 +3,12 @@ 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_identity::services::IdentityServices; +use agent_issuance::{services::IssuanceServices, startup_commands::startup_commands}; 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, - from_jsonwebtoken_algorithm_to_jwsalgorithm, - linked_verifiable_presentation::create_linked_verifiable_presentation_resource, - metadata::{load_metadata, Metadata}, -}; +use agent_shared::config::{config, LogFormat}; use agent_store::{in_memory, postgres, EventPublisher}; use agent_verification::services::VerificationServices; -use axum::{http, response::IntoResponse, routing::get, Json}; -use identity_document::service::{Service, ServiceEndpoint}; use std::sync::Arc; use tokio::{fs, io}; use tower_http::cors::CorsLayer; @@ -37,6 +30,7 @@ async fn main() -> io::Result<()> { secret_manager: Arc::new(tokio::sync::Mutex::new(secret_manager().await)), }); + let identity_services = Arc::new(IdentityServices::new(subject.clone())); 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())); @@ -44,35 +38,39 @@ async fn main() -> io::Result<()> { // 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 identity_event_publishers: Vec> = vec![Box::new(EventPublisherHttp::load().unwrap())]; 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, 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, - ), - }; + let (identity_state, issuance_state, holder_state, verification_state) = + match agent_shared::config::config().event_store.type_ { + agent_shared::config::EventStoreType::Postgres => ( + postgres::identity_state(identity_services, identity_event_publishers).await, + 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::identity_state(identity_services, identity_event_publishers).await, + 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, + ), + }; info!("{:?}", config()); let url = &config().url; - info!("Application url: {:?}", url); - - let url = url::Url::parse(url).unwrap(); + info!("Application url: {}", url); - initialize(&issuance_state, startup_commands(url.clone())).await; + agent_identity::state::initialize(&identity_state).await; + agent_issuance::state::initialize(&issuance_state, startup_commands(url.clone())).await; let mut app = app(ApplicationState { + identity_state: Some(identity_state), issuance_state: Some(issuance_state), holder_state: Some(holder_state), verification_state: Some(verification_state), @@ -84,140 +82,6 @@ async fn main() -> io::Result<()> { app = app.layer(CorsLayer::permissive()); } - // did:web - let enable_did_web = config() - .did_methods - .get(&SupportedDidMethod::Web) - .unwrap_or(&ToggleOptions::default()) - .enabled; - - let did_document = if enable_did_web { - let mut secret_manager = subject.secret_manager.lock().await; - - Some( - secret_manager - .produce_document( - did_manager::DidMethod::Web, - Some(did_manager::MethodSpecificParameters::Web { origin: url.origin() }), - from_jsonwebtoken_algorithm_to_jwsalgorithm( - &agent_shared::config::get_preferred_signing_algorithm(), - ), - ) - .await - .unwrap(), - ) - } else { - None - }; - - // Domain Linkage - let did_configuration_resource = if config().domain_linkage_enabled { - let secret_manager = subject.secret_manager.lock().await; - - Some( - create_did_configuration_resource( - url.clone(), - did_document - .clone() - .expect("No DID document found to create a DID Configuration Resource for"), - &secret_manager, - ) - .await - .expect("Failed to create DID Configuration Resource"), - ) - } else { - None - }; - - // Linked Verifiable Presentation - let linked_verifiable_presentation_resource = - if config!("linked_verifiable_presentation_enabled", bool).unwrap_or(false) { - Some( - create_linked_verifiable_presentation_resource( - url.clone(), - config!("linked_verifiable_credential", String) - .expect("`AGENT_CONFIG_LINKED_VERIFIABLE_CREDENTIAL` is not set") - .into(), - did_document - .clone() - .expect("No DID document found to create a Linked Verifiable Presentation Resource for"), - secret_manager().await, - ) - .await - .expect("Failed to create DID Configuration Resource"), - ) - } else { - None - }; - - if let Some(mut did_document) = did_document { - if let Some(did_configuration_resource) = did_configuration_resource { - // Create a new service and add it to the DID document. - let service = Service::builder(Default::default()) - .id(format!("{}#service-1", did_document.id()).parse().unwrap()) - .type_("LinkedDomains") - .service_endpoint( - serde_json::from_value::(serde_json::json!( - { - "origins": [url.origin().ascii_serialization()] - } - )) - .unwrap(), - ) - .build() - .expect("Failed to create DID Configuration Resource"); - did_document - .insert_service(service) - .expect("Service already exists in DID Document"); - - let path = "/.well-known/did-configuration.json"; - info!("Serving DID Configuration (Domain Linkage) at `{path}`"); - app = app.route(path, get(Json(did_configuration_resource))); - } - - if let Some(linked_verifiable_presentation_resource) = linked_verifiable_presentation_resource { - // Create a new service and add it to the DID document. - - // FIX THISS - let path = "/linked-verifiable-presentation.jwt"; - - let service = Service::builder(Default::default()) - .id(format!("{}#service-2", did_document.id()).parse().unwrap()) - .type_("LinkedVerifiablePresentation") - .service_endpoint( - serde_json::from_value::(serde_json::json!( - { - "origins": [format!("{}{path}", url.origin().ascii_serialization())] - } - )) - .unwrap(), - ) - .build() - .expect("Failed to create Linked Verifiable Presentation Resource"); - did_document - .insert_service(service) - .expect("Service already exists in DID Document"); - - let linked_verifiable_presentation = linked_verifiable_presentation_resource.as_str().to_string(); - - info!("Serving Linked Verifiable Presentation at `{path}`"); - app = app.route( - path, - get(|| async { - ( - [(http::header::CONTENT_TYPE, "application/jwt")], - linked_verifiable_presentation, - ) - .into_response() - }), - ); - } - - let path = "/.well-known/did.json"; - info!("Serving `did:web` document at `{path}`"); - app = app.route(path, get(Json(did_document))); - } - // This is used to indicate that the server accepts requests. // In a docker container this file can be searched to see if its ready. // A better solution can be made later (needed for impierce-demo) diff --git a/agent_holder/Cargo.toml b/agent_holder/Cargo.toml index 974ada58..01bed51b 100644 --- a/agent_holder/Cargo.toml +++ b/agent_holder/Cargo.toml @@ -9,7 +9,10 @@ agent_shared = { path = "../agent_shared" } agent_secret_manager = { path = "../agent_secret_manager" } async-trait.workspace = true +base64.workspace = true cqrs-es.workspace = true +identity_credential.workspace = true +identity_verification.workspace = true jsonwebtoken.workspace = true oid4vci.workspace = true oid4vc-core.workspace = true diff --git a/agent_holder/src/lib.rs b/agent_holder/src/lib.rs index 671c165e..2e9bc9a1 100644 --- a/agent_holder/src/lib.rs +++ b/agent_holder/src/lib.rs @@ -1,4 +1,5 @@ pub mod credential; pub mod offer; +pub mod presentation; pub mod services; pub mod state; diff --git a/agent_holder/src/presentation/README.md b/agent_holder/src/presentation/README.md new file mode 100644 index 00000000..562e6ae0 --- /dev/null +++ b/agent_holder/src/presentation/README.md @@ -0,0 +1,3 @@ +# Presentation + +This aggregate holds everything related to a presentation: diff --git a/agent_holder/src/presentation/aggregate.rs b/agent_holder/src/presentation/aggregate.rs new file mode 100644 index 00000000..226345d4 --- /dev/null +++ b/agent_holder/src/presentation/aggregate.rs @@ -0,0 +1,117 @@ +use super::{command::PresentationCommand, error::PresentationError, event::PresentationEvent}; +use crate::services::HolderServices; +use agent_shared::config::{get_preferred_did_method, get_preferred_signing_algorithm}; +use async_trait::async_trait; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use cqrs_es::Aggregate; +use identity_credential::credential::Jwt; +use jsonwebtoken::{Algorithm, Header}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tracing::info; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Presentation { + // FIX THISS + pub presentation_id: String, + // pub credential_ids: Vec, + pub signed_presentation: Option, +} + +#[async_trait] +impl Aggregate for Presentation { + type Command = PresentationCommand; + type Event = PresentationEvent; + type Error = PresentationError; + type Services = Arc; + + fn aggregate_type() -> String { + "presentation".to_string() + } + + async fn handle(&self, command: Self::Command, services: &Self::Services) -> Result, Self::Error> { + use PresentationCommand::*; + use PresentationError::*; + use PresentationEvent::*; + + info!("Handling command: {:?}", command); + + match command { + CreatePresentation { + presentation_id, + signed_credentials, + } => { + let holder = &services.holder; + let subject_did = holder + .identifier( + get_preferred_did_method().to_string().as_ref(), + get_preferred_signing_algorithm(), + ) + .await + .unwrap(); + + let mut presentation_builder = identity_credential::presentation::Presentation::builder( + subject_did.parse().unwrap(), + Default::default(), + ); + for signed_credential in signed_credentials { + presentation_builder = presentation_builder.credential(signed_credential); + } + + let verifiable_presentation: identity_credential::presentation::Presentation = + presentation_builder.build().unwrap(); + + let payload = verifiable_presentation + .serialize_jwt(&Default::default()) + .expect("FIX THISS"); + + // Compose JWT + let header = Header { + alg: Algorithm::ES256, + typ: Some("JWT".to_string()), + kid: Some(format!("{subject_did}#key-0")), + ..Default::default() + }; + + let message = [ + URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap().as_slice()), + // FIX THISS? + URL_SAFE_NO_PAD.encode(payload.as_bytes()), + ] + .join("."); + + let proof_value = holder + .sign( + &message, + get_preferred_did_method().to_string().as_ref(), + get_preferred_signing_algorithm(), + ) + .await + .unwrap(); + let signature = URL_SAFE_NO_PAD.encode(proof_value.as_slice()); + let message = [message, signature].join("."); + + Ok(vec![PresentationCreated { + presentation_id, + signed_presentation: Jwt::from(message), + }]) + } + } + } + + fn apply(&mut self, event: Self::Event) { + use PresentationEvent::*; + + info!("Applying event: {:?}", event); + + match event { + PresentationCreated { + presentation_id, + signed_presentation, + } => { + self.presentation_id = presentation_id; + self.signed_presentation.replace(signed_presentation); + } + } + } +} diff --git a/agent_holder/src/presentation/command.rs b/agent_holder/src/presentation/command.rs new file mode 100644 index 00000000..367e0f92 --- /dev/null +++ b/agent_holder/src/presentation/command.rs @@ -0,0 +1,11 @@ +use identity_credential::credential::Jwt; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum PresentationCommand { + CreatePresentation { + presentation_id: String, + signed_credentials: Vec, + }, +} diff --git a/agent_holder/src/presentation/error.rs b/agent_holder/src/presentation/error.rs new file mode 100644 index 00000000..fa861056 --- /dev/null +++ b/agent_holder/src/presentation/error.rs @@ -0,0 +1,4 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum PresentationError {} diff --git a/agent_holder/src/presentation/event.rs b/agent_holder/src/presentation/event.rs new file mode 100644 index 00000000..05e552b8 --- /dev/null +++ b/agent_holder/src/presentation/event.rs @@ -0,0 +1,26 @@ +use cqrs_es::DomainEvent; +use identity_credential::credential::Jwt; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum PresentationEvent { + PresentationCreated { + presentation_id: String, + signed_presentation: Jwt, + }, +} + +impl DomainEvent for PresentationEvent { + fn event_type(&self) -> String { + use PresentationEvent::*; + + let event_type: &str = match self { + PresentationCreated { .. } => "PresentationCreated", + }; + event_type.to_string() + } + + fn event_version(&self) -> String { + "1".to_string() + } +} diff --git a/agent_holder/src/presentation/mod.rs b/agent_holder/src/presentation/mod.rs new file mode 100644 index 00000000..7cbc4ed7 --- /dev/null +++ b/agent_holder/src/presentation/mod.rs @@ -0,0 +1,5 @@ +pub mod aggregate; +pub mod command; +pub mod error; +pub mod event; +pub mod views; diff --git a/agent_holder/src/presentation/views/all_presentations.rs b/agent_holder/src/presentation/views/all_presentations.rs new file mode 100644 index 00000000..37a3382d --- /dev/null +++ b/agent_holder/src/presentation/views/all_presentations.rs @@ -0,0 +1,23 @@ +use super::PresentationView; +use crate::presentation::aggregate::Presentation; +use cqrs_es::{EventEnvelope, View}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct AllPresentationsView { + #[serde(flatten)] + pub presentations: HashMap, +} + +impl View for AllPresentationsView { + fn update(&mut self, event: &EventEnvelope) { + self.presentations + // Get the entry for the aggregate_id + .entry(event.aggregate_id.clone()) + // or insert a new one if it doesn't exist + .or_default() + // update the view with the event + .update(event); + } +} diff --git a/agent_holder/src/presentation/views/mod.rs b/agent_holder/src/presentation/views/mod.rs new file mode 100644 index 00000000..190738fa --- /dev/null +++ b/agent_holder/src/presentation/views/mod.rs @@ -0,0 +1,22 @@ +pub mod all_presentations; + +use super::aggregate::Presentation; +use cqrs_es::{EventEnvelope, View}; + +pub type PresentationView = Presentation; + +impl View for Presentation { + fn update(&mut self, event: &EventEnvelope) { + use crate::presentation::event::PresentationEvent::*; + + match &event.payload { + PresentationCreated { + presentation_id, + signed_presentation, + } => { + self.presentation_id.clone_from(presentation_id); + self.signed_presentation.replace(signed_presentation.clone()); + } + } + } +} diff --git a/agent_holder/src/state.rs b/agent_holder/src/state.rs index 2ebbddf4..2d5996b9 100644 --- a/agent_holder/src/state.rs +++ b/agent_holder/src/state.rs @@ -8,6 +8,9 @@ use crate::credential::queries::CredentialView; use crate::offer::aggregate::Offer; use crate::offer::queries::all_offers::AllOffersView; use crate::offer::queries::OfferView; +use crate::presentation::aggregate::Presentation; +use crate::presentation::views::all_presentations::AllPresentationsView; +use crate::presentation::views::PresentationView; #[derive(Clone)] pub struct HolderState { @@ -19,6 +22,7 @@ pub struct HolderState { #[derive(Clone)] pub struct CommandHandlers { pub credential: CommandHandler, + pub presentation: CommandHandler, pub offer: CommandHandler, } @@ -28,19 +32,25 @@ pub struct CommandHandlers { type Queries = ViewRepositories< dyn ViewRepository, dyn ViewRepository, + dyn ViewRepository, + dyn ViewRepository, dyn ViewRepository, dyn ViewRepository, >; -pub struct ViewRepositories +pub struct ViewRepositories where C1: ViewRepository + ?Sized, C2: ViewRepository + ?Sized, + P1: ViewRepository + ?Sized, + P2: ViewRepository + ?Sized, O1: ViewRepository + ?Sized, O2: ViewRepository + ?Sized, { pub credential: Arc, pub all_credentials: Arc, + pub presentation: Arc, + pub all_presentations: Arc, pub offer: Arc, pub all_offers: Arc, } @@ -50,6 +60,8 @@ impl Clone for Queries { ViewRepositories { credential: self.credential.clone(), all_credentials: self.all_credentials.clone(), + presentation: self.presentation.clone(), + all_presentations: self.all_presentations.clone(), offer: self.offer.clone(), all_offers: self.all_offers.clone(), } diff --git a/agent_identity/Cargo.toml b/agent_identity/Cargo.toml new file mode 100644 index 00000000..dbd286ce --- /dev/null +++ b/agent_identity/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "agent_identity" +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 +base64.workspace = true +cqrs-es.workspace = true +derivative = "2.2" +did_manager.workspace = true +identity_credential.workspace = true +identity_core = "1.3" +identity_document = { version = "1.3" } +identity_verification.workspace = true +jsonwebtoken.workspace = true +oid4vci.workspace = true +oid4vc-core.workspace = true +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true +tracing.workspace = true + +[features] +test_utils = [] diff --git a/agent_identity/src/document/README.md b/agent_identity/src/document/README.md new file mode 100644 index 00000000..95ac6726 --- /dev/null +++ b/agent_identity/src/document/README.md @@ -0,0 +1,3 @@ +# Document + +This aggregate holds everything related to a document: diff --git a/agent_identity/src/document/aggregate.rs b/agent_identity/src/document/aggregate.rs new file mode 100644 index 00000000..1313f438 --- /dev/null +++ b/agent_identity/src/document/aggregate.rs @@ -0,0 +1,89 @@ +use std::sync::Arc; + +use agent_shared::{ + config::{config, get_preferred_signing_algorithm}, + from_jsonwebtoken_algorithm_to_jwsalgorithm, +}; +use async_trait::async_trait; +use cqrs_es::Aggregate; +use did_manager::{DidMethod, MethodSpecificParameters}; +use identity_document::document::CoreDocument; +use serde::{Deserialize, Serialize}; +use tracing::info; + +use crate::services::IdentityServices; + +use super::{command::DocumentCommand, error::DocumentError, event::DocumentEvent}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Document { + pub document: Option, +} + +#[async_trait] +impl Aggregate for Document { + type Command = DocumentCommand; + type Event = DocumentEvent; + type Error = DocumentError; + type Services = Arc; + + fn aggregate_type() -> String { + "credential".to_string() + } + + async fn handle(&self, command: Self::Command, services: &Self::Services) -> Result, Self::Error> { + use DocumentCommand::*; + use DocumentError::*; + use DocumentEvent::*; + + info!("Handling command: {:?}", command); + + match command { + CreateDocument { did_method } => { + let mut secret_manager = services.subject.secret_manager.lock().await; + + let method_specific_parameters = + matches!(did_method, DidMethod::Web).then(|| MethodSpecificParameters::Web { + origin: config().url.origin(), + }); + + let document = secret_manager + .produce_document( + did_method, + method_specific_parameters, + // TODO: This way the Document can only support on single algorithm. We need to support multiple algorithms. + from_jsonwebtoken_algorithm_to_jwsalgorithm(&get_preferred_signing_algorithm()), + ) + .await + // FIX THISS + .unwrap(); + + Ok(vec![DocumentCreated { document }]) + } + AddService { service } => { + // FIX THIS + let mut document = self.document.clone().unwrap(); + + // FIX THIS + document.insert_service(service).unwrap(); + + Ok(vec![ServiceAdded { document }]) + } + } + } + + fn apply(&mut self, event: Self::Event) { + use DocumentEvent::*; + + info!("Applying event: {:?}", event); + + match event { + DocumentCreated { document } => { + self.document.replace(document); + } + ServiceAdded { document } => { + self.document.replace(document); + } + } + } +} diff --git a/agent_identity/src/document/command.rs b/agent_identity/src/document/command.rs new file mode 100644 index 00000000..afbc0852 --- /dev/null +++ b/agent_identity/src/document/command.rs @@ -0,0 +1,10 @@ +use did_manager::DidMethod; +use identity_document::service::Service as DocumentService; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum DocumentCommand { + CreateDocument { did_method: DidMethod }, + AddService { service: DocumentService }, +} diff --git a/agent_identity/src/document/error.rs b/agent_identity/src/document/error.rs new file mode 100644 index 00000000..7311b89f --- /dev/null +++ b/agent_identity/src/document/error.rs @@ -0,0 +1,4 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum DocumentError {} diff --git a/agent_identity/src/document/event.rs b/agent_identity/src/document/event.rs new file mode 100644 index 00000000..94e52fbb --- /dev/null +++ b/agent_identity/src/document/event.rs @@ -0,0 +1,25 @@ +use cqrs_es::DomainEvent; +use identity_document::document::CoreDocument; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum DocumentEvent { + DocumentCreated { document: CoreDocument }, + ServiceAdded { document: CoreDocument }, +} + +impl DomainEvent for DocumentEvent { + fn event_type(&self) -> String { + use DocumentEvent::*; + + let event_type: &str = match self { + DocumentCreated { .. } => "DocumentCreated", + ServiceAdded { .. } => "ServiceAdded", + }; + event_type.to_string() + } + + fn event_version(&self) -> String { + "1".to_string() + } +} diff --git a/agent_identity/src/document/mod.rs b/agent_identity/src/document/mod.rs new file mode 100644 index 00000000..7cbc4ed7 --- /dev/null +++ b/agent_identity/src/document/mod.rs @@ -0,0 +1,5 @@ +pub mod aggregate; +pub mod command; +pub mod error; +pub mod event; +pub mod views; diff --git a/agent_identity/src/document/views/mod.rs b/agent_identity/src/document/views/mod.rs new file mode 100644 index 00000000..ff550ab8 --- /dev/null +++ b/agent_identity/src/document/views/mod.rs @@ -0,0 +1,18 @@ +use super::aggregate::Document; +use cqrs_es::{EventEnvelope, View}; + +pub type DocumentView = Document; +impl View for Document { + fn update(&mut self, event: &EventEnvelope) { + use crate::document::event::DocumentEvent::*; + + match &event.payload { + DocumentCreated { document, .. } => { + self.document.replace(document.clone()); + } + ServiceAdded { document, .. } => { + self.document.replace(document.clone()); + } + } + } +} diff --git a/agent_identity/src/lib.rs b/agent_identity/src/lib.rs new file mode 100644 index 00000000..f2de33fd --- /dev/null +++ b/agent_identity/src/lib.rs @@ -0,0 +1,6 @@ +// Aggregates +pub mod document; +pub mod service; + +pub mod services; +pub mod state; diff --git a/agent_identity/src/service/README.md b/agent_identity/src/service/README.md new file mode 100644 index 00000000..cd8d147a --- /dev/null +++ b/agent_identity/src/service/README.md @@ -0,0 +1,3 @@ +# Service + +This aggregate holds everything related to a service: diff --git a/agent_identity/src/service/aggregate.rs b/agent_identity/src/service/aggregate.rs new file mode 100644 index 00000000..b1b4bf02 --- /dev/null +++ b/agent_identity/src/service/aggregate.rs @@ -0,0 +1,200 @@ +use super::{command::ServiceCommand, error::ServiceError, event::ServiceEvent}; +use crate::services::IdentityServices; +use agent_shared::{ + config::{config, get_preferred_signing_algorithm}, + from_jsonwebtoken_algorithm_to_jwsalgorithm, +}; +use async_trait::async_trait; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use cqrs_es::Aggregate; +use did_manager::{DidMethod, MethodSpecificParameters}; +use identity_core::{ + common::{Duration, Timestamp}, + convert::FromJson, +}; +use identity_credential::{ + credential::Jwt, + domain_linkage::{DomainLinkageConfiguration, DomainLinkageCredentialBuilder}, +}; +use identity_document::service::{Service as DocumentService, ServiceEndpoint}; +use jsonwebtoken::Header; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::sync::Arc; +use tracing::info; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ServiceResource { + DomainLinkage(DomainLinkageConfiguration), +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Service { + pub id: String, + pub service: Option, + pub resource: Option, +} + +#[async_trait] +impl Aggregate for Service { + type Command = ServiceCommand; + type Event = ServiceEvent; + type Error = ServiceError; + type Services = Arc; + + fn aggregate_type() -> String { + "service".to_string() + } + + async fn handle(&self, command: Self::Command, services: &Self::Services) -> Result, Self::Error> { + use ServiceCommand::*; + use ServiceError::*; + use ServiceEvent::*; + + info!("Handling command: {:?}", command); + + match command { + CreateDomainLinkageService { service_id } => { + let mut secret_manager = services.subject.secret_manager.lock().await; + + let origin = config().url.origin(); + let method_specific_parameters = MethodSpecificParameters::Web { origin: origin.clone() }; + + // TODO: implement for all non-deterministic methods and not just DID WEB + let document = secret_manager + .produce_document( + DidMethod::Web, + Some(method_specific_parameters), + // TODO: This way the Document can only support on single algorithm. We need to support multiple algorithms. + from_jsonwebtoken_algorithm_to_jwsalgorithm(&get_preferred_signing_algorithm()), + ) + .await + // FIX THISS + .unwrap(); + + let subject_did = document.id(); + + let origin = identity_core::common::Url::parse(origin.ascii_serialization()).unwrap(); + let domain_linkage_credential = DomainLinkageCredentialBuilder::new() + .issuer(subject_did.clone()) + .origin(origin.clone()) + .issuance_date(Timestamp::now_utc()) + // Expires after a year. + .expiration_date(Timestamp::now_utc().checked_add(Duration::days(365)).unwrap()) + .build() + // FIX THISS + .unwrap() + .serialize_jwt(Default::default()) + // FIX THISS + .unwrap(); + + // Compose JWT + let header = Header { + alg: get_preferred_signing_algorithm(), + typ: Some("JWT".to_string()), + // TODO: make dynamic + kid: Some(format!("{subject_did}#key-0")), + ..Default::default() + }; + + let message = [ + URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap().as_slice()), + URL_SAFE_NO_PAD.encode(domain_linkage_credential.as_bytes()), + ] + .join("."); + + let proof_value = secret_manager + .sign( + message.as_bytes(), + from_jsonwebtoken_algorithm_to_jwsalgorithm(&get_preferred_signing_algorithm()), + ) + .await + .unwrap(); + let signature = URL_SAFE_NO_PAD.encode(proof_value.as_slice()); + let message = [message, signature].join("."); + + let domain_linkage_configuration = DomainLinkageConfiguration::new(vec![Jwt::from(message)]); + info!("Configuration Resource >>: {domain_linkage_configuration:#}"); + + // Create a new service and add it to the DID document. + let service = DocumentService::builder(Default::default()) + .id(format!("{subject_did}#{service_id}").parse().unwrap()) + .type_("LinkedDomains") + .service_endpoint( + ServiceEndpoint::from_json_value(json!({ + "origins": [origin] + })) + .unwrap(), + ) + .build() + .expect("Failed to create DID Configuration Resource"); + + Ok(vec![DomainLinkageServiceCreated { + service_id, + service, + resource: ServiceResource::DomainLinkage(domain_linkage_configuration), + }]) + } + CreateLinkedVerifiablePresentationService { + service_id, + presentation_id, + } => { + let mut secret_manager = services.subject.secret_manager.lock().await; + + let origin = config().url.origin(); + let method_specific_parameters = MethodSpecificParameters::Web { origin: origin.clone() }; + let origin = identity_core::common::Url::parse(origin.ascii_serialization()).unwrap(); + + // TODO: implement for all non-deterministic methods and not just DID WEB + let document = secret_manager + .produce_document( + DidMethod::Web, + Some(method_specific_parameters), + // TODO: This way the Document can only support on single algorithm. We need to support multiple algorithms. + from_jsonwebtoken_algorithm_to_jwsalgorithm(&get_preferred_signing_algorithm()), + ) + .await + // FIX THISS + .unwrap(); + + let subject_did = document.id(); + + let service = DocumentService::builder(Default::default()) + .id(format!("{subject_did}#{service_id}").parse().unwrap()) + .type_("LinkedVerifiablePresentation") + .service_endpoint(ServiceEndpoint::from( + // FIX THIS + format!("{origin}v0/holder/presentations/{presentation_id}/signed") + .parse::() + .unwrap(), + )) + .build() + .expect("Failed to create Linked Verifiable Presentation Resource"); + + Ok(vec![LinkedVerifiablePresentationServiceCreated { service_id, service }]) + } + } + } + + fn apply(&mut self, event: Self::Event) { + use ServiceEvent::*; + + info!("Applying event: {:?}", event); + + match event { + DomainLinkageServiceCreated { + service_id, + service, + resource, + } => { + self.id = service_id; + self.service.replace(service); + self.resource.replace(resource); + } + LinkedVerifiablePresentationServiceCreated { service_id, service } => { + self.id = service_id; + self.service.replace(service); + } + } + } +} diff --git a/agent_identity/src/service/command.rs b/agent_identity/src/service/command.rs new file mode 100644 index 00000000..6078b06b --- /dev/null +++ b/agent_identity/src/service/command.rs @@ -0,0 +1,13 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum ServiceCommand { + CreateDomainLinkageService { + service_id: String, + }, + CreateLinkedVerifiablePresentationService { + service_id: String, + presentation_id: String, + }, +} diff --git a/agent_identity/src/service/error.rs b/agent_identity/src/service/error.rs new file mode 100644 index 00000000..c85a9221 --- /dev/null +++ b/agent_identity/src/service/error.rs @@ -0,0 +1,4 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ServiceError {} diff --git a/agent_identity/src/service/event.rs b/agent_identity/src/service/event.rs new file mode 100644 index 00000000..685c1cc1 --- /dev/null +++ b/agent_identity/src/service/event.rs @@ -0,0 +1,37 @@ +use cqrs_es::DomainEvent; +use derivative::Derivative; +use identity_document::service::Service as DocumentService; +use serde::{Deserialize, Serialize}; + +use super::aggregate::ServiceResource; + +#[derive(Clone, Debug, Deserialize, Serialize, Derivative)] +#[derivative(PartialEq)] +pub enum ServiceEvent { + DomainLinkageServiceCreated { + service_id: String, + service: DocumentService, + #[derivative(PartialEq = "ignore")] + resource: ServiceResource, + }, + LinkedVerifiablePresentationServiceCreated { + service_id: String, + service: DocumentService, + }, +} + +impl DomainEvent for ServiceEvent { + fn event_type(&self) -> String { + use ServiceEvent::*; + + let event_type: &str = match self { + DomainLinkageServiceCreated { .. } => "DomainLinkageServiceCreated", + LinkedVerifiablePresentationServiceCreated { .. } => "LinkedVerifiablePresentationServiceCreated", + }; + event_type.to_string() + } + + fn event_version(&self) -> String { + "1".to_string() + } +} diff --git a/agent_identity/src/service/mod.rs b/agent_identity/src/service/mod.rs new file mode 100644 index 00000000..7cbc4ed7 --- /dev/null +++ b/agent_identity/src/service/mod.rs @@ -0,0 +1,5 @@ +pub mod aggregate; +pub mod command; +pub mod error; +pub mod event; +pub mod views; diff --git a/agent_identity/src/service/views/mod.rs b/agent_identity/src/service/views/mod.rs new file mode 100644 index 00000000..e7e4bad1 --- /dev/null +++ b/agent_identity/src/service/views/mod.rs @@ -0,0 +1,25 @@ +use super::aggregate::Service; +use cqrs_es::{EventEnvelope, View}; + +pub type ServiceView = Service; +impl View for Service { + fn update(&mut self, event: &EventEnvelope) { + use crate::service::event::ServiceEvent::*; + + match &event.payload { + DomainLinkageServiceCreated { + service_id, + service, + resource, + } => { + self.id.clone_from(service_id); + self.service.replace(service.clone()); + self.resource.replace(resource.clone()); + } + LinkedVerifiablePresentationServiceCreated { service_id, service } => { + self.id.clone_from(service_id); + self.service.replace(service.clone()); + } + } + } +} diff --git a/agent_identity/src/services.rs b/agent_identity/src/services.rs new file mode 100644 index 00000000..c6602927 --- /dev/null +++ b/agent_identity/src/services.rs @@ -0,0 +1,13 @@ +use agent_secret_manager::subject::Subject; +use std::sync::Arc; + +/// Identity services. This struct is used to sign credentials and validate credential requests. +pub struct IdentityServices { + pub subject: Arc, +} + +impl IdentityServices { + pub fn new(subject: Arc) -> Self { + Self { subject } + } +} diff --git a/agent_identity/src/state.rs b/agent_identity/src/state.rs new file mode 100644 index 00000000..d470f43b --- /dev/null +++ b/agent_identity/src/state.rs @@ -0,0 +1,107 @@ +use agent_shared::config::{config, SupportedDidMethod, ToggleOptions}; +use agent_shared::handlers::command_handler; +use agent_shared::{application_state::CommandHandler, handlers::query_handler}; +use cqrs_es::persist::ViewRepository; +use did_manager::DidMethod; +use std::sync::Arc; +use tracing::{info, warn}; + +use crate::document::command::DocumentCommand; +use crate::{ + document::{aggregate::Document, views::DocumentView}, + service::{aggregate::Service, command::ServiceCommand, views::ServiceView}, +}; + +#[derive(Clone)] +pub struct IdentityState { + pub command: CommandHandlers, + pub query: Queries, +} + +/// The command handlers are used to execute commands on the aggregates. +#[derive(Clone)] +pub struct CommandHandlers { + pub document: CommandHandler, + pub service: 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 + D: ViewRepository + ?Sized, + S: ViewRepository + ?Sized, +{ + pub document: Arc, + pub service: Arc, +} + +impl Clone for Queries { + fn clone(&self) -> Self { + ViewRepositories { + document: self.document.clone(), + service: self.service.clone(), + } + } +} + +/// The unique identifier for the linked domain service. +pub const DOMAIN_LINKAGE_SERVICE_ID: &str = "linked-domain-service"; + +/// Initialize the identity state. +pub async fn initialize(state: &IdentityState) { + info!("Initializing ..."); + + // did:web + let enable_did_web = config() + .did_methods + .get(&SupportedDidMethod::Web) + .unwrap_or(&ToggleOptions::default()) + .enabled; + + if enable_did_web { + let did_method = DidMethod::Web; + let command = DocumentCommand::CreateDocument { + did_method: did_method.clone(), + }; + + if command_handler(&did_method.to_string(), &state.command.document, command) + .await + .is_err() + { + warn!("Failed to create document"); + } + + if config().domain_linkage_enabled { + let command = ServiceCommand::CreateDomainLinkageService { + service_id: DOMAIN_LINKAGE_SERVICE_ID.to_string(), + }; + + if command_handler(DOMAIN_LINKAGE_SERVICE_ID, &state.command.service, command) + .await + .is_err() + { + warn!("Failed to create domain linkage service"); + } + + let service = query_handler(&DOMAIN_LINKAGE_SERVICE_ID, &state.query.service) + .await + .unwrap() + .unwrap() + .service + .unwrap(); + + let command = DocumentCommand::AddService { service }; + + if command_handler(&did_method.to_string(), &state.command.document, command) + .await + .is_err() + { + warn!("Failed to add service to document"); + } + } + } +} diff --git a/agent_issuance/src/credential/aggregate.rs b/agent_issuance/src/credential/aggregate.rs index b64ee7da..652b896d 100644 --- a/agent_issuance/src/credential/aggregate.rs +++ b/agent_issuance/src/credential/aggregate.rs @@ -193,19 +193,31 @@ impl Aggregate for Credential { // Insert the rest of the fields for (key, value) in credential_subject { - new_credential_subject.insert(key, value); + if key != "id" { + new_credential_subject.insert(key, value); + } } + info!("Credential subject: {:?}", new_credential_subject); + // Replace the original credentialSubject with the new map credential.raw["credentialSubject"] = serde_json::Value::Object(new_credential_subject); + info!("Credential: {:?}", credential); + #[cfg(feature = "test_utils")] let iat = 0; #[cfg(not(feature = "test_utils"))] - let iat = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) + let iat = credential.raw["issuanceDate"] + .as_str() + .unwrap() + .parse::>() .unwrap() - .as_secs() as i64; + .timestamp(); + // let iat = std::time::SystemTime::now() + // .duration_since(std::time::UNIX_EPOCH) + // .unwrap() + // .as_secs() as i64; json!(jwt::encode( services.issuer.clone(), diff --git a/agent_secret_manager/src/subject.rs b/agent_secret_manager/src/subject.rs index 304aef9b..a96e5ae1 100644 --- a/agent_secret_manager/src/subject.rs +++ b/agent_secret_manager/src/subject.rs @@ -141,7 +141,7 @@ impl oid4vc_core::Subject for Subject { } fn origin() -> url::Origin { - config().url.parse::().unwrap().origin() + config().url.origin() } #[cfg(test)] diff --git a/agent_shared/src/config.rs b/agent_shared/src/config.rs index e08b3a4b..74ad495f 100644 --- a/agent_shared/src/config.rs +++ b/agent_shared/src/config.rs @@ -18,7 +18,7 @@ use url::Url; pub struct ApplicationConfiguration { pub log_format: LogFormat, pub event_store: EventStoreConfig, - pub url: String, + pub url: Url, pub base_path: Option, pub cors_enabled: Option, pub did_methods: HashMap, diff --git a/agent_shared/src/domain_linkage/mod.rs b/agent_shared/src/domain_linkage/mod.rs deleted file mode 100644 index 7bd5d82c..00000000 --- a/agent_shared/src/domain_linkage/mod.rs +++ /dev/null @@ -1,123 +0,0 @@ -pub mod verifiable_credential_jwt; - -use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; -use std::time::{SystemTime, UNIX_EPOCH}; - -use crate::config::get_preferred_signing_algorithm; -use crate::error::SharedError; -use crate::from_jsonwebtoken_algorithm_to_jwsalgorithm; -use did_manager::SecretManager; -use identity_core::common::{Duration, Timestamp}; -use identity_credential::credential::{Credential, Jwt}; -use identity_credential::domain_linkage::{DomainLinkageConfiguration, DomainLinkageCredentialBuilder}; -use identity_did::DID; -use identity_document::document::CoreDocument; -use identity_storage::{JwkDocumentExt, JwsSignatureOptions, Storage}; -use jsonwebtoken::Header; -use tracing::info; -use verifiable_credential_jwt::VerifiableCredentialJwt; - -pub async fn create_did_configuration_resource( - url: url::Url, - did_document: CoreDocument, - secret_manager: &SecretManager, -) -> Result { - let url = if cfg!(feature = "local_development") { - url::Url::parse("http://local.example.org:8080").unwrap() - } else { - url - }; - - let origin = identity_core::common::Url::parse(url.origin().ascii_serialization()) - .map_err(|e| SharedError::Generic(e.to_string()))?; - let domain_linkage_credential: Credential = DomainLinkageCredentialBuilder::new() - .issuer(did_document.id().clone()) - .origin(origin) - .issuance_date(Timestamp::now_utc()) - // Expires after a year. - .expiration_date( - Timestamp::now_utc() - .checked_add(Duration::days(365)) - .ok_or_else(|| SharedError::Generic("calculation should not overflow".to_string()))?, - ) - .build() - .map_err(|e| SharedError::Generic(e.to_string()))?; - - info!("Domain Linkage Credential: {domain_linkage_credential:#}"); - - // Construct a `Storage` (identity_stronghold) for temporary usage: create JWS, etc. - let key_storage = secret_manager.stronghold_storage.clone(); - let key_id_storage = secret_manager.stronghold_storage.clone(); - - let storage = Storage::new(key_storage, key_id_storage); - - info!("DID Document: {did_document:#}"); - - // identity.rs currently doesn't know how to handle a `did:web` document in `create_credential_jwt()`. - - // Compose JWT and sign - let jwt: Jwt = match did_document.id().method() { - "iota" => did_document - .create_credential_jwt( - &domain_linkage_credential, - &storage, - // TODO: make this dynamic - "key-0", - &JwsSignatureOptions::default(), - None, - ) - .await - .map_err(|e| SharedError::Generic(e.to_string()))?, - "web" => { - let subject_did = did_document.id().to_string(); - let issuer_did = subject_did.clone(); - - let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64; - let expires_in_secs = 60 * 60 * 24 * 365; - - // Create a new verifiable credential. - let payload = VerifiableCredentialJwt::builder() - .sub(&subject_did) - .iss(&issuer_did) - .nbf(now) - .exp(now + expires_in_secs) - .verifiable_credential(serde_json::json!(domain_linkage_credential)) - .build() - .unwrap(); - - // Compose JWT - let header = Header { - alg: get_preferred_signing_algorithm(), - typ: Some("JWT".to_string()), - kid: Some(format!("{subject_did}#key-0")), - ..Default::default() - }; - - let message = [ - URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap().as_slice()), - URL_SAFE_NO_PAD.encode(serde_json::to_vec(&payload).unwrap().as_slice()), - ] - .join("."); - - let proof_value = secret_manager - .sign( - message.as_bytes(), - from_jsonwebtoken_algorithm_to_jwsalgorithm(&crate::config::get_preferred_signing_algorithm()), - ) - .await - .unwrap(); - let signature = URL_SAFE_NO_PAD.encode(proof_value.as_slice()); - let message = [message, signature].join("."); - - Jwt::from(message) - } - _ => { - unimplemented!("Unsupported DID method: {}", did_document.id().method()); - } - }; - - let configuration_resource: DomainLinkageConfiguration = DomainLinkageConfiguration::new(vec![jwt]); - println!("Configuration Resource >>: {configuration_resource:#}"); - - Ok(configuration_resource) -} diff --git a/agent_shared/src/domain_linkage/verifiable_credential_jwt.rs b/agent_shared/src/domain_linkage/verifiable_credential_jwt.rs deleted file mode 100644 index a16aeb43..00000000 --- a/agent_shared/src/domain_linkage/verifiable_credential_jwt.rs +++ /dev/null @@ -1,82 +0,0 @@ -use is_empty::IsEmpty; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; - -use crate::error::SharedError; - -/// Set of IANA registered claims by the Internet Engineering Task Force (IETF) in -/// [RFC 7519](https://tools.ietf.org/html/rfc7519#section-4.1). -#[skip_serializing_none] -#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Clone, IsEmpty)] -pub struct RFC7519Claims { - pub iss: Option, - pub sub: Option, - pub aud: Option, - pub exp: Option, - pub nbf: Option, - pub iat: Option, - pub jti: Option, -} - -// Macro that generates a builder function for a field. -#[macro_export] -macro_rules! builder_fn { - ($name:ident, $ty:ty) => { - #[allow(clippy::should_implement_trait)] - pub fn $name(mut self, value: impl Into<$ty>) -> Self { - self.$name.replace(value.into()); - self - } - }; - ($field:ident, $name:ident, $ty:ty) => { - #[allow(clippy::should_implement_trait)] - pub fn $name(mut self, value: impl Into<$ty>) -> Self { - self.$field.$name.replace(value.into()); - self - } - }; -} - -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -pub struct VerifiableCredentialJwt { - #[serde(flatten)] - pub rfc7519_claims: RFC7519Claims, - #[serde(rename = "vc")] - pub verifiable_credential: serde_json::Value, -} - -impl VerifiableCredentialJwt { - pub fn builder() -> VerifiableCredentialJwtBuilder { - VerifiableCredentialJwtBuilder::new() - } -} - -#[derive(Default)] -pub struct VerifiableCredentialJwtBuilder { - rfc7519_claims: RFC7519Claims, - verifiable_credential: Option, -} - -impl VerifiableCredentialJwtBuilder { - pub fn new() -> Self { - VerifiableCredentialJwtBuilder::default() - } - - pub fn build(self) -> Result { - Ok(VerifiableCredentialJwt { - rfc7519_claims: self.rfc7519_claims, - verifiable_credential: self - .verifiable_credential - .ok_or(SharedError::Generic("`verifiable_credential` is required".to_string()))?, - }) - } - - builder_fn!(rfc7519_claims, iss, String); - builder_fn!(rfc7519_claims, sub, String); - builder_fn!(rfc7519_claims, aud, String); - builder_fn!(rfc7519_claims, exp, i64); - builder_fn!(rfc7519_claims, nbf, i64); - builder_fn!(rfc7519_claims, iat, i64); - builder_fn!(rfc7519_claims, jti, String); - builder_fn!(verifiable_credential, serde_json::Value); -} diff --git a/agent_shared/src/lib.rs b/agent_shared/src/lib.rs index 08835acd..e183167c 100644 --- a/agent_shared/src/lib.rs +++ b/agent_shared/src/lib.rs @@ -1,11 +1,9 @@ pub mod application_state; pub mod config; pub mod custom_queries; -pub mod domain_linkage; pub mod error; pub mod generic_query; pub mod handlers; -pub mod linked_verifiable_presentation; pub mod url_utils; pub use ::config::ConfigError; diff --git a/agent_shared/src/linked_verifiable_presentation.rs b/agent_shared/src/linked_verifiable_presentation.rs deleted file mode 100644 index 1fad1c22..00000000 --- a/agent_shared/src/linked_verifiable_presentation.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::error::SharedError; -use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; -use did_manager::SecretManager; -use identity_credential::{credential::Jwt, presentation::Presentation}; -use identity_document::document::CoreDocument; -use jsonwebtoken::{Algorithm, Header}; - -pub async fn create_linked_verifiable_presentation_resource( - url: url::Url, - verifiable_credential_jwt: Jwt, - did_document: CoreDocument, - secret_manager: SecretManager, -) -> Result { - let presentation = Presentation::builder(url.into(), identity_core::common::Object::new()) - .credential(Jwt::from(verifiable_credential_jwt)) - .build() - .map_err(|e| SharedError::Generic(e.to_string()))?; - - let payload = presentation.serialize_jwt(&Default::default()).expect("FIX THISS"); - - // TODO: make distinction between different DID methods. - let subject_did = did_document.id().to_string(); - - // Compose JWT - let header = Header { - alg: Algorithm::EdDSA, - typ: Some("JWT".to_string()), - kid: Some(format!("{subject_did}#key-0")), - ..Default::default() - }; - - let message = [ - URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap().as_slice()), - URL_SAFE_NO_PAD.encode(payload.as_bytes()), - ] - .join("."); - - let proof_value = secret_manager.sign(message.as_bytes()).await.unwrap(); - let signature = URL_SAFE_NO_PAD.encode(proof_value.as_slice()); - let message = [message, signature].join("."); - - Ok(Jwt::from(message)) -} diff --git a/agent_store/Cargo.toml b/agent_store/Cargo.toml index 90b86f95..b48261ae 100644 --- a/agent_store/Cargo.toml +++ b/agent_store/Cargo.toml @@ -6,6 +6,7 @@ rust-version.workspace = true [dependencies] agent_holder = { path = "../agent_holder" } +agent_identity = { path = "../agent_identity" } 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 5e5fef7d..1f4dd535 100644 --- a/agent_store/src/in_memory.rs +++ b/agent_store/src/in_memory.rs @@ -1,5 +1,6 @@ -use crate::{partition_event_publishers, EventPublisher}; +use crate::{partition_event_publishers, EventPublisher, Partitions}; use agent_holder::{services::HolderServices, state::HolderState}; +use agent_identity::{services::IdentityServices, state::IdentityState}; use agent_issuance::{ offer::{ aggregate::Offer, @@ -9,7 +10,7 @@ use agent_issuance::{ }, }, services::IssuanceServices, - state::{IssuanceState, ViewRepositories}, + state::IssuanceState, SimpleLoggingQuery, }; use agent_shared::{application_state::Command, custom_queries::ListAllQuery, generic_query::generic_query}; @@ -115,6 +116,44 @@ where } } +pub async fn identity_state( + identity_services: Arc, + event_publishers: Vec>, +) -> IdentityState { + // Initialize the in-memory repositories. + let document = Arc::new(MemRepository::default()); + let service = Arc::new(MemRepository::default()); + + // Partition the event_publishers into the different aggregates. + let Partitions { + document_event_publishers, + service_event_publishers, + .. + } = partition_event_publishers(event_publishers); + + IdentityState { + command: agent_identity::state::CommandHandlers { + document: Arc::new( + document_event_publishers.into_iter().fold( + AggregateHandler::new(identity_services.clone()) + .append_query(SimpleLoggingQuery {}) + .append_query(generic_query(document.clone())), + |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), + ), + ), + service: Arc::new( + service_event_publishers.into_iter().fold( + AggregateHandler::new(identity_services) + .append_query(SimpleLoggingQuery {}) + .append_query(generic_query(service.clone())), + |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), + ), + ), + }, + query: agent_identity::state::ViewRepositories { document, service }, + } +} + pub async fn issuance_state( issuance_services: Arc, event_publishers: Vec>, @@ -131,8 +170,12 @@ 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, _, _, _, _) = - partition_event_publishers(event_publishers); + let Partitions { + server_config_event_publishers, + credential_event_publishers, + offer_event_publishers, + .. + } = partition_event_publishers(event_publishers); IssuanceState { command: agent_issuance::state::CommandHandlers { @@ -163,7 +206,7 @@ pub async fn issuance_state( ), ), }, - query: ViewRepositories { + query: agent_issuance::state::ViewRepositories { server_config, credential, offer, @@ -180,21 +223,28 @@ pub async fn holder_state( // Initialize the in-memory repositories. let credential = Arc::new(MemRepository::default()); let offer = Arc::new(MemRepository::default()); + let presentation = Arc::new(MemRepository::default()); let all_credentials = Arc::new(MemRepository::default()); let all_offers = Arc::new(MemRepository::default()); + let all_presentations = Arc::new(MemRepository::default()); // Create custom-queries for the offer aggregate. let all_credentials_query = ListAllQuery::new(all_credentials.clone(), "all_credentials"); let all_offers_query = ListAllQuery::new(all_offers.clone(), "all_offers"); + let all_presentations_query = ListAllQuery::new(all_presentations.clone(), "all_presentations"); // Partition the event_publishers into the different aggregates. - let (_, _, _, credential_event_publishers, offer_event_publishers, _, _) = - partition_event_publishers(event_publishers); + let Partitions { + holder_credential_event_publishers, + presentation_event_publishers, + received_offer_event_publishers, + .. + } = partition_event_publishers(event_publishers); HolderState { command: agent_holder::state::CommandHandlers { credential: Arc::new( - credential_event_publishers.into_iter().fold( + holder_credential_event_publishers.into_iter().fold( AggregateHandler::new(holder_services.clone()) .append_query(SimpleLoggingQuery {}) .append_query(generic_query(credential.clone())) @@ -202,8 +252,17 @@ pub async fn holder_state( |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), ), ), + presentation: Arc::new( + presentation_event_publishers.into_iter().fold( + AggregateHandler::new(holder_services.clone()) + .append_query(SimpleLoggingQuery {}) + .append_query(generic_query(presentation.clone())) + .append_query(all_presentations_query), + |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), + ), + ), offer: Arc::new( - offer_event_publishers.into_iter().fold( + received_offer_event_publishers.into_iter().fold( AggregateHandler::new(holder_services.clone()) .append_query(SimpleLoggingQuery {}) .append_query(generic_query(offer.clone())) @@ -215,6 +274,8 @@ pub async fn holder_state( query: agent_holder::state::ViewRepositories { credential, all_credentials, + presentation, + all_presentations, offer, all_offers, }, @@ -230,8 +291,11 @@ 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) = - partition_event_publishers(event_publishers); + let Partitions { + authorization_request_event_publishers, + connection_event_publishers, + .. + } = partition_event_publishers(event_publishers); VerificationState { command: agent_verification::state::CommandHandlers { diff --git a/agent_store/src/lib.rs b/agent_store/src/lib.rs index b5baaf80..d7af1002 100644 --- a/agent_store/src/lib.rs +++ b/agent_store/src/lib.rs @@ -1,3 +1,4 @@ +use agent_identity::{document::aggregate::Document, service::aggregate::Service}; use agent_issuance::{ credential::aggregate::Credential, offer::aggregate::Offer, server_config::aggregate::ServerConfig, }; @@ -7,30 +8,44 @@ use cqrs_es::Query; pub mod in_memory; pub mod postgres; +pub type DocumentEventPublisher = Box>; +pub type ServiceEventPublisher = Box>; pub type ServerConfigEventPublisher = Box>; pub type CredentialEventPublisher = Box>; pub type OfferEventPublisher = Box>; pub type HolderCredentialEventPublisher = Box>; +pub type PresentationEventPublisher = Box>; pub type ReceivedOfferEventPublisher = Box>; pub type AuthorizationRequestEventPublisher = Box>; pub type ConnectionEventPublisher = Box>; /// Contains all the event_publishers for each aggregate. -pub type Partitions = ( - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, -); +#[derive(Default)] +pub struct Partitions { + pub document_event_publishers: Vec, + pub service_event_publishers: Vec, + pub server_config_event_publishers: Vec, + pub credential_event_publishers: Vec, + pub offer_event_publishers: Vec, + pub holder_credential_event_publishers: Vec, + pub presentation_event_publishers: Vec, + pub received_offer_event_publishers: Vec, + pub authorization_request_event_publishers: Vec, + pub connection_event_publishers: Vec, +} /// An outbound event_publisher is a component that listens to events and dispatches them to the appropriate service. For each /// aggregate, by default, `None` is returned. If an event_publisher is interested in a specific aggregate, it should return a /// `Some` with the appropriate query. // TODO: move this to a separate crate that will include all the logic for event_publishers, i.e. `agent_event_publisher`. pub trait EventPublisher { + fn document(&mut self) -> Option { + None + } + fn service(&mut self) -> Option { + None + } + fn server_config(&mut self) -> Option { None } @@ -44,6 +59,9 @@ pub trait EventPublisher { fn holder_credential(&mut self) -> Option { None } + fn presentation(&mut self) -> Option { + None + } fn received_offer(&mut self) -> Option { None } @@ -57,35 +75,43 @@ 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![]), - |mut partitions, mut event_publisher| { + event_publishers + .into_iter() + .fold(Partitions::default(), |mut partitions, mut event_publisher| { + if let Some(document) = event_publisher.document() { + partitions.document_event_publishers.push(document); + } + if let Some(service) = event_publisher.service() { + partitions.service_event_publishers.push(service); + } + if let Some(server_config) = event_publisher.server_config() { - partitions.0.push(server_config); + partitions.server_config_event_publishers.push(server_config); } if let Some(credential) = event_publisher.credential() { - partitions.1.push(credential); + partitions.credential_event_publishers.push(credential); } if let Some(offer) = event_publisher.offer() { - partitions.2.push(offer); + partitions.offer_event_publishers.push(offer); } - if let Some(credential) = event_publisher.holder_credential() { - partitions.3.push(credential); + if let Some(holder_credential) = event_publisher.holder_credential() { + partitions.holder_credential_event_publishers.push(holder_credential); } - if let Some(offer) = event_publisher.received_offer() { - partitions.4.push(offer); + if let Some(received_offer) = event_publisher.received_offer() { + partitions.received_offer_event_publishers.push(received_offer); } if let Some(authorization_request) = event_publisher.authorization_request() { - partitions.5.push(authorization_request); + partitions + .authorization_request_event_publishers + .push(authorization_request); } if let Some(connection) = event_publisher.connection() { - partitions.6.push(connection); + partitions.connection_event_publishers.push(connection); } partitions - }, - ) + }) } #[cfg(test)] @@ -140,20 +166,26 @@ mod test { let event_publishers: Vec> = vec![Box::new(FooEventPublisher), Box::new(BarEventPublisher)]; - let ( + let Partitions { + document_event_publishers, + service_event_publishers, server_config_event_publishers, credential_event_publishers, offer_event_publishers, holder_credential_event_publishers, + presentation_event_publishers, received_offer_event_publishers, authorization_request_event_publishers, connection_event_publishers, - ) = partition_event_publishers(event_publishers); + } = partition_event_publishers(event_publishers); + assert_eq!(document_event_publishers.len(), 0); + assert_eq!(service_event_publishers.len(), 0); assert_eq!(server_config_event_publishers.len(), 1); assert_eq!(credential_event_publishers.len(), 0); assert_eq!(offer_event_publishers.len(), 0); assert_eq!(holder_credential_event_publishers.len(), 0); + assert_eq!(presentation_event_publishers.len(), 0); assert_eq!(received_offer_event_publishers.len(), 0); assert_eq!(authorization_request_event_publishers.len(), 0); assert_eq!(connection_event_publishers.len(), 2); diff --git a/agent_store/src/postgres.rs b/agent_store/src/postgres.rs index 5a91de7e..973ff042 100644 --- a/agent_store/src/postgres.rs +++ b/agent_store/src/postgres.rs @@ -1,9 +1,10 @@ -use crate::{partition_event_publishers, EventPublisher}; -use agent_holder::{services::HolderServices, state::HolderState}; +use crate::{partition_event_publishers, EventPublisher, Partitions}; +use agent_holder::{presentation::views::all_presentations, services::HolderServices, state::HolderState}; +use agent_identity::{services::IdentityServices, state::IdentityState}; use agent_issuance::{ offer::queries::{access_token::AccessTokenQuery, pre_authorized_code::PreAuthorizedCodeQuery}, services::IssuanceServices, - state::{CommandHandlers, IssuanceState, ViewRepositories}, + state::IssuanceState, SimpleLoggingQuery, }; use agent_shared::{ @@ -65,6 +66,49 @@ where } } +pub async fn identity_state( + identity_services: Arc, + event_publishers: Vec>, +) -> IdentityState { + 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 postgres repositories. + let document = Arc::new(PostgresViewRepository::new("document", pool.clone())); + let service = Arc::new(PostgresViewRepository::new("service", pool.clone())); + + // Partition the event_publishers into the different aggregates. + let Partitions { + document_event_publishers, + service_event_publishers, + .. + } = partition_event_publishers(event_publishers); + + IdentityState { + command: agent_identity::state::CommandHandlers { + document: Arc::new( + document_event_publishers.into_iter().fold( + AggregateHandler::new(pool.clone(), identity_services.clone()) + .append_query(SimpleLoggingQuery {}) + .append_query(generic_query(document.clone())), + |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), + ), + ), + service: Arc::new( + service_event_publishers.into_iter().fold( + AggregateHandler::new(pool.clone(), identity_services) + .append_query(SimpleLoggingQuery {}) + .append_query(generic_query(service.clone())), + |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), + ), + ), + }, + query: agent_identity::state::ViewRepositories { document, service }, + } +} + pub async fn issuance_state( issuance_services: Arc, event_publishers: Vec>, @@ -86,11 +130,15 @@ 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, _, _, _, _) = - partition_event_publishers(event_publishers); + let Partitions { + server_config_event_publishers, + credential_event_publishers, + offer_event_publishers, + .. + } = 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(pool.clone(), ()) @@ -118,7 +166,7 @@ pub async fn issuance_state( ), ), }, - query: ViewRepositories { + query: agent_issuance::state::ViewRepositories { server_config, credential, offer, @@ -140,23 +188,32 @@ pub async fn holder_state( // Initialize the postgres repositories. let credential: Arc> = Arc::new(PostgresViewRepository::new("holder_credential", pool.clone())); + let presentation: Arc> = + Arc::new(PostgresViewRepository::new("presentation", pool.clone())); + let offer = Arc::new(PostgresViewRepository::new("received_offer", 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_presentations: Arc> = + Arc::new(PostgresViewRepository::new("all_presentations", pool.clone())); let all_offers = Arc::new(PostgresViewRepository::new("all_offers", pool.clone())); // Create custom-queries for the offer aggregate. let all_credentials_query = ListAllQuery::new(all_credentials.clone(), "all_credentials"); + let all_presentations_query = ListAllQuery::new(all_presentations.clone(), "all_presentations"); 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, _, _) = - partition_event_publishers(event_publishers); + let Partitions { + holder_credential_event_publishers, + presentation_event_publishers, + received_offer_event_publishers, + .. + } = partition_event_publishers(event_publishers); HolderState { command: agent_holder::state::CommandHandlers { credential: Arc::new( - credential_event_publishers.into_iter().fold( + holder_credential_event_publishers.into_iter().fold( AggregateHandler::new(pool.clone(), holder_services.clone()) .append_query(SimpleLoggingQuery {}) .append_query(generic_query(credential.clone())) @@ -164,8 +221,17 @@ pub async fn holder_state( |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), ), ), + presentation: Arc::new( + presentation_event_publishers.into_iter().fold( + AggregateHandler::new(pool.clone(), holder_services.clone()) + .append_query(SimpleLoggingQuery {}) + .append_query(generic_query(presentation.clone())) + .append_query(all_presentations_query), + |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), + ), + ), offer: Arc::new( - offer_event_publishers.into_iter().fold( + received_offer_event_publishers.into_iter().fold( AggregateHandler::new(pool, holder_services.clone()) .append_query(SimpleLoggingQuery {}) .append_query(generic_query(offer.clone())) @@ -177,6 +243,8 @@ pub async fn holder_state( query: agent_holder::state::ViewRepositories { credential, all_credentials, + presentation, + all_presentations, offer, all_offers, }, @@ -197,8 +265,11 @@ pub async fn verification_state( let connection = Arc::new(PostgresViewRepository::new("connection", pool.clone())); // Partition the event_publishers into the different aggregates. - let (_, _, _, _, _, authorization_request_event_publishers, connection_event_publishers) = - partition_event_publishers(event_publishers); + let Partitions { + authorization_request_event_publishers, + connection_event_publishers, + .. + } = partition_event_publishers(event_publishers); VerificationState { command: agent_verification::state::CommandHandlers { diff --git a/agent_verification/src/authorization_request/aggregate.rs b/agent_verification/src/authorization_request/aggregate.rs index bb14c130..25cce297 100644 --- a/agent_verification/src/authorization_request/aggregate.rs +++ b/agent_verification/src/authorization_request/aggregate.rs @@ -51,8 +51,8 @@ impl Aggregate for AuthorizationRequest { .unwrap(); let url = &config().url; - let request_uri = format!("{url}/request/{state}").parse().unwrap(); - let redirect_uri = format!("{url}/redirect").parse::().unwrap(); + let request_uri = format!("{url}request/{state}").parse().unwrap(); + let redirect_uri = format!("{url}redirect").parse::().unwrap(); let authorization_request = Box::new(if let Some(presentation_definition) = presentation_definition { GenericAuthorizationRequest::OID4VP(Box::new( From 1bf0c723906fb34906eaea4b0ae375bfd66f7cf8 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Wed, 18 Sep 2024 16:07:59 +0200 Subject: [PATCH 32/68] style: use consistent nameing for `View` variables --- agent_api_rest/src/holder/holder/credentials/mod.rs | 2 +- agent_api_rest/src/holder/holder/offers/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/agent_api_rest/src/holder/holder/credentials/mod.rs b/agent_api_rest/src/holder/holder/credentials/mod.rs index 5e91880c..c18e9f71 100644 --- a/agent_api_rest/src/holder/holder/credentials/mod.rs +++ b/agent_api_rest/src/holder/holder/credentials/mod.rs @@ -11,7 +11,7 @@ use serde_json::json; #[axum_macros::debug_handler] pub(crate) async fn credentials(State(state): State) -> Response { match query_handler("all_credentials", &state.query.all_credentials).await { - Ok(Some(offer_view)) => (StatusCode::OK, Json(offer_view)).into_response(), + Ok(Some(all_credentials_view)) => (StatusCode::OK, Json(all_credentials_view)).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 c513aecd..a5130017 100644 --- a/agent_api_rest/src/holder/holder/offers/mod.rs +++ b/agent_api_rest/src/holder/holder/offers/mod.rs @@ -14,7 +14,7 @@ use serde_json::json; #[axum_macros::debug_handler] pub(crate) async fn offers(State(state): State) -> Response { match query_handler("all_offers", &state.query.all_offers).await { - Ok(Some(offer_view)) => (StatusCode::OK, Json(offer_view)).into_response(), + Ok(Some(all_offers_view)) => (StatusCode::OK, Json(all_offers_view)).into_response(), Ok(None) => (StatusCode::OK, Json(json!({}))).into_response(), _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } From 961fb2f7eefd686d1e77781422d73d724c0908d8 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Wed, 18 Sep 2024 16:51:00 +0200 Subject: [PATCH 33/68] style: rename variables --- .../holder/holder/presentations/presentation_signed.rs | 2 +- agent_holder/src/credential/aggregate.rs | 10 +++++----- agent_holder/src/presentation/aggregate.rs | 4 ++-- agent_holder/src/presentation/views/mod.rs | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/agent_api_rest/src/holder/holder/presentations/presentation_signed.rs b/agent_api_rest/src/holder/holder/presentations/presentation_signed.rs index 7a597ced..cc5dbc3a 100644 --- a/agent_api_rest/src/holder/holder/presentations/presentation_signed.rs +++ b/agent_api_rest/src/holder/holder/presentations/presentation_signed.rs @@ -13,7 +13,7 @@ pub(crate) async fn presentation_signed( ) -> Response { match query_handler(&presentation_id, &state.query.presentation).await { Ok(Some(Presentation { - signed_presentation: Some(signed_presentation), + signed: Some(signed_presentation), .. })) => ( StatusCode::OK, diff --git a/agent_holder/src/credential/aggregate.rs b/agent_holder/src/credential/aggregate.rs index 5488ccf1..d99f2438 100644 --- a/agent_holder/src/credential/aggregate.rs +++ b/agent_holder/src/credential/aggregate.rs @@ -4,6 +4,7 @@ use crate::credential::event::CredentialEvent; use crate::services::HolderServices; use async_trait::async_trait; use cqrs_es::Aggregate; +use identity_credential::credential::Jwt; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tracing::info; @@ -12,7 +13,7 @@ use tracing::info; pub struct Credential { pub credential_id: Option, pub offer_id: Option, - pub credential: Option, + pub signed: Option, } #[async_trait] @@ -62,7 +63,7 @@ impl Aggregate for Credential { } => { self.credential_id = Some(credential_id); self.offer_id = Some(offer_id); - self.credential = Some(credential); + self.signed = Some(credential); } } } @@ -79,7 +80,6 @@ pub mod credential_tests { use agent_secret_manager::service::Service; use cqrs_es::test::TestFramework; use rstest::rstest; - use serde_json::json; type CredentialTestFramework = TestFramework; @@ -91,12 +91,12 @@ pub mod credential_tests { .when(CredentialCommand::AddCredential { credential_id: credential_id.clone(), offer_id: offer_id.clone(), - credential: json!(OPENBADGE_VERIFIABLE_CREDENTIAL_JWT), + credential: Jwt::from(OPENBADGE_VERIFIABLE_CREDENTIAL_JWT.to_string()), }) .then_expect_events(vec![CredentialEvent::CredentialAdded { credential_id, offer_id, - credential: json!(OPENBADGE_VERIFIABLE_CREDENTIAL_JWT), + credential: Jwt::from(OPENBADGE_VERIFIABLE_CREDENTIAL_JWT.to_string()), }]) } } diff --git a/agent_holder/src/presentation/aggregate.rs b/agent_holder/src/presentation/aggregate.rs index 226345d4..bc5b3760 100644 --- a/agent_holder/src/presentation/aggregate.rs +++ b/agent_holder/src/presentation/aggregate.rs @@ -15,7 +15,7 @@ pub struct Presentation { // FIX THISS pub presentation_id: String, // pub credential_ids: Vec, - pub signed_presentation: Option, + pub signed: Option, } #[async_trait] @@ -110,7 +110,7 @@ impl Aggregate for Presentation { signed_presentation, } => { self.presentation_id = presentation_id; - self.signed_presentation.replace(signed_presentation); + self.signed.replace(signed_presentation); } } } diff --git a/agent_holder/src/presentation/views/mod.rs b/agent_holder/src/presentation/views/mod.rs index 190738fa..d46b743c 100644 --- a/agent_holder/src/presentation/views/mod.rs +++ b/agent_holder/src/presentation/views/mod.rs @@ -15,7 +15,7 @@ impl View for Presentation { signed_presentation, } => { self.presentation_id.clone_from(presentation_id); - self.signed_presentation.replace(signed_presentation.clone()); + self.signed.replace(signed_presentation.clone()); } } } From e4783b3d7f8887f6fbbc29fa842ad5bd52bb4284 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Wed, 18 Sep 2024 16:52:04 +0200 Subject: [PATCH 34/68] refactor: use `type` for `View`s to reduce code duplication --- agent_holder/src/credential/queries/mod.rs | 10 ++-------- agent_holder/src/offer/queries/mod.rs | 18 ++---------------- 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/agent_holder/src/credential/queries/mod.rs b/agent_holder/src/credential/queries/mod.rs index 007f0255..0e3cf0d5 100644 --- a/agent_holder/src/credential/queries/mod.rs +++ b/agent_holder/src/credential/queries/mod.rs @@ -3,14 +3,8 @@ pub mod all_credentials; use super::event::CredentialEvent; use crate::credential::aggregate::Credential; use cqrs_es::{EventEnvelope, View}; -use serde::{Deserialize, Serialize}; -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct CredentialView { - pub credential_id: Option, - pub offer_id: Option, - pub credential: Option, -} +pub type CredentialView = Credential; impl View for CredentialView { fn update(&mut self, event: &EventEnvelope) { @@ -24,7 +18,7 @@ impl View for CredentialView { } => { self.credential_id.replace(credential_id.clone()); self.offer_id.replace(offer_id.clone()); - self.credential.replace(credential.clone()); + self.signed.replace(credential.clone()); } } } diff --git a/agent_holder/src/offer/queries/mod.rs b/agent_holder/src/offer/queries/mod.rs index ad13bde1..b88a47d3 100644 --- a/agent_holder/src/offer/queries/mod.rs +++ b/agent_holder/src/offer/queries/mod.rs @@ -1,25 +1,11 @@ pub mod all_offers; -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 std::collections::HashMap; -#[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, -} +pub type OfferView = Offer; -impl View for OfferView { +impl View for Offer { fn update(&mut self, event: &EventEnvelope) { use crate::offer::event::OfferEvent::*; From a20cffeaa96afd4bc386f708049940518992d75f Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Wed, 18 Sep 2024 16:52:48 +0200 Subject: [PATCH 35/68] refactor: use `Jwt` instead of `Value` --- agent_holder/src/credential/command.rs | 3 ++- agent_holder/src/credential/event.rs | 3 ++- agent_holder/src/offer/aggregate.rs | 11 +++++++---- agent_holder/src/offer/event.rs | 3 ++- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/agent_holder/src/credential/command.rs b/agent_holder/src/credential/command.rs index af839527..97d83718 100644 --- a/agent_holder/src/credential/command.rs +++ b/agent_holder/src/credential/command.rs @@ -1,3 +1,4 @@ +use identity_credential::credential::Jwt; use serde::Deserialize; #[derive(Debug, Deserialize)] @@ -6,6 +7,6 @@ pub enum CredentialCommand { AddCredential { credential_id: String, offer_id: String, - credential: serde_json::Value, + credential: Jwt, }, } diff --git a/agent_holder/src/credential/event.rs b/agent_holder/src/credential/event.rs index cbc50106..40e7c9eb 100644 --- a/agent_holder/src/credential/event.rs +++ b/agent_holder/src/credential/event.rs @@ -1,4 +1,5 @@ use cqrs_es::DomainEvent; +use identity_credential::credential::Jwt; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] @@ -6,7 +7,7 @@ pub enum CredentialEvent { CredentialAdded { credential_id: String, offer_id: String, - credential: serde_json::Value, + credential: Jwt, }, } diff --git a/agent_holder/src/offer/aggregate.rs b/agent_holder/src/offer/aggregate.rs index 47239bdf..0debd371 100644 --- a/agent_holder/src/offer/aggregate.rs +++ b/agent_holder/src/offer/aggregate.rs @@ -4,6 +4,7 @@ use crate::offer::event::OfferEvent; use crate::services::HolderServices; use async_trait::async_trait; use cqrs_es::Aggregate; +use identity_credential::credential::Jwt; use oid4vci::credential_issuer::credential_configurations_supported::CredentialConfigurationsSupportedObject; use oid4vci::credential_offer::{CredentialOffer, CredentialOfferParameters, Grants}; use oid4vci::credential_response::CredentialResponseType; @@ -32,7 +33,7 @@ pub struct Offer { // 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 credentials: Vec, } #[async_trait] @@ -175,7 +176,7 @@ impl Aggregate for Offer { .as_ref() .ok_or(MissingCredentialConfigurationsError)?; - let credentials: Vec = match credential_configuration_ids.len() { + let credentials: Vec = match credential_configuration_ids.len() { 0 => vec![], 1 => { let credential_configuration_id = &credential_configuration_ids[0]; @@ -191,7 +192,9 @@ impl Aggregate for Offer { .map_err(|_| CredentialResponseError)?; let credential = match credential_response.credential { - CredentialResponseType::Immediate { credential, .. } => credential, + CredentialResponseType::Immediate { credential, .. } => { + Jwt::from(credential.as_str().unwrap().to_string()) + } CredentialResponseType::Deferred { .. } => { return Err(UnsupportedDeferredCredentialResponseError) } @@ -444,7 +447,7 @@ pub mod tests { .then_expect_events(vec![OfferEvent::CredentialResponseReceived { offer_id: offer_id.clone(), status: Status::Received, - credentials: vec![json!("eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJkZWdyZWUiOnsidHlwZSI6Ik1hc3RlckRlZ3JlZSIsIm5hbWUiOiJNYXN0ZXIgb2YgT2NlYW5vZ3JhcGh5In0sImZpcnN0X25hbWUiOiJGZXJyaXMiLCJsYXN0X25hbWUiOiJSdXN0YWNlYW4ifSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiJ9fQ.jQEpI7DhjOcmyhPEpfGARwcRyzor_fUvynb43-eqD9175FBoshENX0S-8qlloQ7vbT5gat8TjvcDlGDN720ZBw")], + credentials: vec![Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJkZWdyZWUiOnsidHlwZSI6Ik1hc3RlckRlZ3JlZSIsIm5hbWUiOiJNYXN0ZXIgb2YgT2NlYW5vZ3JhcGh5In0sImZpcnN0X25hbWUiOiJGZXJyaXMiLCJsYXN0X25hbWUiOiJSdXN0YWNlYW4ifSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiJ9fQ.jQEpI7DhjOcmyhPEpfGARwcRyzor_fUvynb43-eqD9175FBoshENX0S-8qlloQ7vbT5gat8TjvcDlGDN720ZBw".to_string())], }]); } diff --git a/agent_holder/src/offer/event.rs b/agent_holder/src/offer/event.rs index 4db40468..4f3753fc 100644 --- a/agent_holder/src/offer/event.rs +++ b/agent_holder/src/offer/event.rs @@ -1,5 +1,6 @@ use super::aggregate::Status; use cqrs_es::DomainEvent; +use identity_credential::credential::Jwt; use oid4vci::{ credential_issuer::credential_configurations_supported::CredentialConfigurationsSupportedObject, credential_offer::CredentialOfferParameters, token_response::TokenResponse, @@ -25,7 +26,7 @@ pub enum OfferEvent { CredentialResponseReceived { offer_id: String, status: Status, - credentials: Vec, + credentials: Vec, }, CredentialOfferRejected { offer_id: String, From 504c3ea31b8c2c2093f392ef0621f79f55eb47b5 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Wed, 18 Sep 2024 16:53:19 +0200 Subject: [PATCH 36/68] feat: add error handling --- .../src/holder/holder/presentations/mod.rs | 13 ++-- agent_api_rest/src/identity/services/mod.rs | 62 ++++++++++++++----- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/agent_api_rest/src/holder/holder/presentations/mod.rs b/agent_api_rest/src/holder/holder/presentations/mod.rs index 5fea998d..a403eaed 100644 --- a/agent_api_rest/src/holder/holder/presentations/mod.rs +++ b/agent_api_rest/src/holder/holder/presentations/mod.rs @@ -40,13 +40,13 @@ pub(crate) async fn post_presentations(State(state): State, Json(pa let mut credentials = vec![]; + // Get all the credentials. for credential_id in credential_ids { match query_handler(&credential_id, &state.query.credential).await { Ok(Some(CredentialView { - credential: Some(credential), + signed: Some(credential), .. })) => { - let credential = Jwt::from(credential.as_str().unwrap().to_string()); credentials.push(credential); } Ok(None) => return StatusCode::NOT_FOUND.into_response(), @@ -61,13 +61,16 @@ pub(crate) async fn post_presentations(State(state): State, Json(pa signed_credentials: credentials, }; - command_handler(&presentation_id, &state.command.presentation, command) + // Create the presentation. + if command_handler(&presentation_id, &state.command.presentation, command) .await - .unwrap(); + .is_err() + { + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } match query_handler(&presentation_id, &state.query.presentation).await { Ok(Some(presentation_view)) => (StatusCode::OK, Json(presentation_view)).into_response(), - Ok(None) => (StatusCode::OK, Json(json!({}))).into_response(), _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } } diff --git a/agent_api_rest/src/identity/services/mod.rs b/agent_api_rest/src/identity/services/mod.rs index 8f08603f..04d5985d 100644 --- a/agent_api_rest/src/identity/services/mod.rs +++ b/agent_api_rest/src/identity/services/mod.rs @@ -1,14 +1,34 @@ -use agent_identity::{document::command::DocumentCommand, service::command::ServiceCommand, state::IdentityState}; +use agent_identity::{ + document::command::DocumentCommand, + service::{aggregate::Service, command::ServiceCommand}, + state::IdentityState, +}; use agent_shared::handlers::{command_handler, query_handler}; use axum::{ - extract::{Path, State}, + extract::State, response::{IntoResponse, Response}, + Json, }; use did_manager::DidMethod; use hyper::StatusCode; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tracing::info; + +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LinkedVPEndpointRequest { + pub presentation_id: String, +} #[axum_macros::debug_handler] -pub(crate) async fn linked_vp(State(state): State, Path(presentation_id): Path) -> Response { +pub(crate) async fn linked_vp(State(state): State, Json(payload): Json) -> Response { + info!("Request Body: {}", payload); + + let Ok(LinkedVPEndpointRequest { presentation_id }) = serde_json::from_value(payload) else { + return (StatusCode::BAD_REQUEST, "invalid payload").into_response(); + }; + let service_id = "linked-verifiable-presentation-service".to_string(); let command = ServiceCommand::CreateLinkedVerifiablePresentationService { service_id: service_id.clone(), @@ -16,22 +36,34 @@ pub(crate) async fn linked_vp(State(state): State, Path(presentat }; // Create a linked verifiable presentation service. - command_handler(&service_id, &state.command.service, command) + if command_handler(&service_id, &state.command.service, command) .await - .unwrap(); + .is_err() + { + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } - let service = query_handler(&service_id, &state.query.service) - .await - .unwrap() - .unwrap() - .service - .unwrap(); + let linked_verifiable_presentation_service = match query_handler(&service_id, &state.query.service).await { + Ok(Some(Service { + service: Some(linked_verifiable_presentation_service), + .. + })) => linked_verifiable_presentation_service, + _ => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), + }; - let command = DocumentCommand::AddService { service }; + let command = DocumentCommand::AddService { + service: linked_verifiable_presentation_service, + }; - command_handler(&DidMethod::Web.to_string(), &state.command.document, command) + if command_handler(&DidMethod::Web.to_string(), &state.command.document, command) .await - .unwrap(); + .is_err() + { + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } - StatusCode::OK.into_response() + match query_handler(&DidMethod::Web.to_string(), &state.query.document).await { + Ok(Some(document)) => (StatusCode::OK, Json(document)).into_response(), + _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } } From 24507cbf3529377cfb93c7fd64829874d21ac78f Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Wed, 18 Sep 2024 16:55:22 +0200 Subject: [PATCH 37/68] fix: remove `presentation_id` from route --- agent_api_rest/src/identity/mod.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/agent_api_rest/src/identity/mod.rs b/agent_api_rest/src/identity/mod.rs index 3e5063c6..40cb1c09 100644 --- a/agent_api_rest/src/identity/mod.rs +++ b/agent_api_rest/src/identity/mod.rs @@ -13,10 +13,7 @@ use crate::API_VERSION; pub fn router(identity_state: IdentityState) -> Router { Router::new() - .nest( - API_VERSION, - Router::new().route("/services/linked-vp/:presentation_id", post(linked_vp)), - ) + .nest(API_VERSION, Router::new().route("/services/linked-vp", post(linked_vp))) .route("/.well-known/did.json", get(did)) .route("/.well-known/did-configuration.json", get(did_configuration)) .with_state(identity_state) From 0cabb68a62cad889caea364306063b217526468e Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Wed, 18 Sep 2024 17:04:15 +0200 Subject: [PATCH 38/68] refactor: add error handling and comments --- agent_api_rest/src/identity/well_known/did.rs | 2 -- .../identity/well_known/did_configuration.rs | 1 - agent_identity/src/state.rs | 24 ++++++++++++------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/agent_api_rest/src/identity/well_known/did.rs b/agent_api_rest/src/identity/well_known/did.rs index c873a369..416d2173 100644 --- a/agent_api_rest/src/identity/well_known/did.rs +++ b/agent_api_rest/src/identity/well_known/did.rs @@ -10,8 +10,6 @@ use hyper::StatusCode; #[axum_macros::debug_handler] pub(crate) async fn did(State(state): State) -> Response { - // TODO: check if enabled - // Get the DID Document if it exists. match query_handler(&DidMethod::Web.to_string(), &state.query.document).await { Ok(Some(DocumentView { document: Some(document), diff --git a/agent_api_rest/src/identity/well_known/did_configuration.rs b/agent_api_rest/src/identity/well_known/did_configuration.rs index 6060ec6e..d246028a 100644 --- a/agent_api_rest/src/identity/well_known/did_configuration.rs +++ b/agent_api_rest/src/identity/well_known/did_configuration.rs @@ -12,7 +12,6 @@ use hyper::StatusCode; #[axum_macros::debug_handler] pub(crate) async fn did_configuration(State(state): State) -> Response { - // TODO: check if enabled // Get the DomainLinkageConfiguration if it exists. match query_handler(DOMAIN_LINKAGE_SERVICE_ID, &state.query.service).await { Ok(Some(ServiceView { diff --git a/agent_identity/src/state.rs b/agent_identity/src/state.rs index d470f43b..dca8c19b 100644 --- a/agent_identity/src/state.rs +++ b/agent_identity/src/state.rs @@ -55,13 +55,13 @@ pub const DOMAIN_LINKAGE_SERVICE_ID: &str = "linked-domain-service"; pub async fn initialize(state: &IdentityState) { info!("Initializing ..."); - // did:web let enable_did_web = config() .did_methods .get(&SupportedDidMethod::Web) .unwrap_or(&ToggleOptions::default()) .enabled; + // If the did:web method is enabled, create a document if enable_did_web { let did_method = DidMethod::Web; let command = DocumentCommand::CreateDocument { @@ -75,6 +75,8 @@ pub async fn initialize(state: &IdentityState) { warn!("Failed to create document"); } + // If domain linkage is enabled, create the domain linkage service and add it to the document. + // TODO: Support this for other (non-deterministic) DID methods. if config().domain_linkage_enabled { let command = ServiceCommand::CreateDomainLinkageService { service_id: DOMAIN_LINKAGE_SERVICE_ID.to_string(), @@ -87,14 +89,20 @@ pub async fn initialize(state: &IdentityState) { warn!("Failed to create domain linkage service"); } - let service = query_handler(&DOMAIN_LINKAGE_SERVICE_ID, &state.query.service) - .await - .unwrap() - .unwrap() - .service - .unwrap(); + let linked_domains_service = match query_handler(&DOMAIN_LINKAGE_SERVICE_ID, &state.query.service).await { + Ok(Some(Service { + service: Some(linked_domains_service), + .. + })) => linked_domains_service, + _ => { + warn!("Failed to retrieve linked domains service"); + return; + } + }; - let command = DocumentCommand::AddService { service }; + let command = DocumentCommand::AddService { + service: linked_domains_service, + }; if command_handler(&did_method.to_string(), &state.command.document, command) .await From 3f24ce40ab1e6e22840bc96ef8139a1e5ee57209 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Wed, 18 Sep 2024 17:14:44 +0200 Subject: [PATCH 39/68] refactor: remove unused dependencies --- Cargo.lock | 6 ------ agent_api_rest/src/lib.rs | 33 +++++++++++++++++++++------------ agent_application/Cargo.toml | 6 ------ agent_application/src/main.rs | 9 +-------- 4 files changed, 22 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb1a9d60..cafd64ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,15 +117,9 @@ dependencies = [ "agent_store", "agent_verification", "axum 0.7.5", - "did_manager", - "identity_credential", - "identity_verification", - "serde_json", "tokio", - "tower-http 0.5.2", "tracing", "tracing-subscriber", - "url", ] [[package]] diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index 6cd05f36..25e6f81d 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -9,8 +9,9 @@ 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, Router}; -use tower_http::trace::TraceLayer; -use tracing::{info_span, Span}; +use std::time::Duration; +use tower_http::{cors::CorsLayer, trace::TraceLayer}; +use tracing::{info, info_span, Span}; pub const API_VERSION: &str = "/v0"; @@ -30,7 +31,7 @@ pub fn app( verification_state, }: ApplicationState, ) -> Router { - Router::new() + let app = Router::new() .nest( &get_base_path().unwrap_or_default(), Router::new() @@ -51,17 +52,25 @@ pub fn app( ) }) .on_request(|request: &Request<_>, _span: &Span| { - tracing::info!("Received request"); - tracing::info!("Request Headers: {:?}", request.headers()); + info!("Received request"); + info!("Request Headers: {:?}", request.headers()); }) - .on_response(|response: &Response, _latency: std::time::Duration, _span: &Span| { - tracing::info!("Returning {}", response.status()); - tracing::info!("Response Headers: {:?}", response.headers()); + .on_response(|response: &Response, _latency: Duration, _span: &Span| { + info!("Returning {}", response.status()); + info!("Response Headers: {:?}", response.headers()); }) - .on_body_chunk(|chunk: &Bytes, _latency: std::time::Duration, _span: &Span| { - tracing::info!("Response Body: {}", std::str::from_utf8(chunk).unwrap()); + .on_body_chunk(|chunk: &Bytes, _latency: Duration, _span: &Span| { + info!("Response Body: {}", std::str::from_utf8(chunk).unwrap()); }), - ) + ); + + // CORS + if config().cors_enabled.unwrap_or(false) { + info!("CORS (permissive) enabled for all routes"); + app.layer(CorsLayer::permissive()) + } else { + app + } } fn get_base_path() -> Result { @@ -82,7 +91,7 @@ fn get_base_path() -> Result { panic!("UNICORE__BASE_PATH can't be empty, remove or set path"); } - tracing::info!("Base path: {:?}", base_path); + info!("Base path: {:?}", base_path); format!("/{}", base_path) }) diff --git a/agent_application/Cargo.toml b/agent_application/Cargo.toml index 246dfce8..078f67cd 100644 --- a/agent_application/Cargo.toml +++ b/agent_application/Cargo.toml @@ -16,12 +16,6 @@ agent_store = { path = "../agent_store" } agent_verification = { path = "../agent_verification" } axum.workspace = true -did_manager.workspace = true -identity_credential.workspace = true -identity_verification.workspace = true -serde_json.workspace = true tokio.workspace = true -tower-http.workspace = true tracing.workspace = true tracing-subscriber.workspace = true -url.workspace = true diff --git a/agent_application/src/main.rs b/agent_application/src/main.rs index 2b01d32e..61932d1d 100644 --- a/agent_application/src/main.rs +++ b/agent_application/src/main.rs @@ -11,7 +11,6 @@ use agent_store::{in_memory, postgres, EventPublisher}; use agent_verification::services::VerificationServices; use std::sync::Arc; use tokio::{fs, io}; -use tower_http::cors::CorsLayer; use tracing::info; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -69,19 +68,13 @@ async fn main() -> io::Result<()> { agent_identity::state::initialize(&identity_state).await; agent_issuance::state::initialize(&issuance_state, startup_commands(url.clone())).await; - let mut app = app(ApplicationState { + let app = app(ApplicationState { identity_state: Some(identity_state), issuance_state: Some(issuance_state), holder_state: Some(holder_state), verification_state: Some(verification_state), }); - // CORS - if config().cors_enabled.unwrap_or(false) { - info!("CORS (permissive) enabled for all routes"); - app = app.layer(CorsLayer::permissive()); - } - // This is used to indicate that the server accepts requests. // In a docker container this file can be searched to see if its ready. // A better solution can be made later (needed for impierce-demo) From 1647121142ce63fa4178462918f486247b8e20e3 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Wed, 18 Sep 2024 20:07:15 +0200 Subject: [PATCH 40/68] build: remove unused dependencies --- Cargo.lock | 26 ---------------------- agent_api_rest/Cargo.toml | 1 - agent_event_publisher_http/Cargo.toml | 1 - agent_holder/Cargo.toml | 1 - agent_identity/Cargo.toml | 3 --- agent_issuance/Cargo.toml | 5 ----- agent_issuance/src/credential/aggregate.rs | 2 +- agent_secret_manager/Cargo.toml | 2 -- agent_shared/Cargo.toml | 12 +--------- 9 files changed, 2 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cafd64ce..b7d4b597 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,7 +96,6 @@ dependencies = [ "tower", "tower-http 0.5.2", "tracing", - "tracing-subscriber", "tracing-test", "url", "uuid", @@ -140,7 +139,6 @@ dependencies = [ "serde", "serde_json", "serde_with 3.8.1", - "serde_yaml", "tokio", "tracing", "wiremock", @@ -163,7 +161,6 @@ dependencies = [ "cqrs-es", "did_manager", "identity_credential", - "identity_verification", "jsonwebtoken", "lazy_static", "mime", @@ -197,10 +194,7 @@ dependencies = [ "identity_core", "identity_credential", "identity_document", - "identity_verification", "jsonwebtoken", - "oid4vc-core", - "oid4vci", "serde", "serde_json", "thiserror", @@ -221,14 +215,11 @@ dependencies = [ "cqrs-es", "derivative", "did_manager", - "futures", "identity_core", "identity_credential", - "jsonschema", "jsonwebtoken", "lazy_static", "oid4vc-core", - "oid4vc-manager", "oid4vci", "once_cell", "reqwest 0.12.5", @@ -237,12 +228,10 @@ dependencies = [ "serde_json", "serial_test", "thiserror", - "tokio", "tracing", "tracing-test", "types-ob-v3", "url", - "uuid", ] [[package]] @@ -253,7 +242,6 @@ dependencies = [ "anyhow", "async-trait", "base64 0.22.1", - "cqrs-es", "did_manager", "futures", "identity_iota", @@ -263,7 +251,6 @@ dependencies = [ "oid4vc-core", "p256 0.13.2", "ring", - "serde", "serde_json", "tokio", "url", @@ -274,19 +261,10 @@ name = "agent_shared" version = "0.1.0" dependencies = [ "async-trait", - "base64 0.22.1", "config", "cqrs-es", - "did_manager", "dotenvy", - "identity_core", - "identity_credential", - "identity_did", - "identity_document", "identity_iota", - "identity_storage", - "identity_verification", - "is_empty", "jsonwebtoken", "oid4vc-core", "oid4vci", @@ -296,7 +274,6 @@ dependencies = [ "serde", "serde_json", "serde_with 3.8.1", - "serde_yaml", "strum 0.26.3", "thiserror", "time", @@ -3451,13 +3428,10 @@ dependencies = [ "identity_document", "identity_iota_core", "identity_verification", - "iota-crypto", - "rand 0.8.5", "seahash", "serde", "serde_json", "thiserror", - "tokio", ] [[package]] diff --git a/agent_api_rest/Cargo.toml b/agent_api_rest/Cargo.toml index fb3bfabc..9390adbf 100644 --- a/agent_api_rest/Cargo.toml +++ b/agent_api_rest/Cargo.toml @@ -27,7 +27,6 @@ siopv2.workspace = true tokio.workspace = true tower-http.workspace = true tracing.workspace = true -tracing-subscriber.workspace = true url.workspace = true uuid.workspace = true diff --git a/agent_event_publisher_http/Cargo.toml b/agent_event_publisher_http/Cargo.toml index a9a0f29d..a9811bf7 100644 --- a/agent_event_publisher_http/Cargo.toml +++ b/agent_event_publisher_http/Cargo.toml @@ -22,7 +22,6 @@ rustls = { version = "0.23", default-features = false, features = [ reqwest.workspace = true serde.workspace = true serde_with.workspace = true -serde_yaml.workspace = true tokio.workspace = true tracing.workspace = true diff --git a/agent_holder/Cargo.toml b/agent_holder/Cargo.toml index 01bed51b..f079e626 100644 --- a/agent_holder/Cargo.toml +++ b/agent_holder/Cargo.toml @@ -12,7 +12,6 @@ async-trait.workspace = true base64.workspace = true cqrs-es.workspace = true identity_credential.workspace = true -identity_verification.workspace = true jsonwebtoken.workspace = true oid4vci.workspace = true oid4vc-core.workspace = true diff --git a/agent_identity/Cargo.toml b/agent_identity/Cargo.toml index dbd286ce..a16f539f 100644 --- a/agent_identity/Cargo.toml +++ b/agent_identity/Cargo.toml @@ -16,10 +16,7 @@ did_manager.workspace = true identity_credential.workspace = true identity_core = "1.3" identity_document = { version = "1.3" } -identity_verification.workspace = true jsonwebtoken.workspace = true -oid4vci.workspace = true -oid4vc-core.workspace = true serde.workspace = true serde_json.workspace = true thiserror.workspace = true diff --git a/agent_issuance/Cargo.toml b/agent_issuance/Cargo.toml index 26be2835..53e35007 100644 --- a/agent_issuance/Cargo.toml +++ b/agent_issuance/Cargo.toml @@ -13,22 +13,17 @@ 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 reqwest.workspace = true serde.workspace = true serde_json.workspace = true thiserror.workspace = true -tokio.workspace = true tracing.workspace = true url.workspace = true -uuid.workspace = true # `test_utils` dependencies lazy_static = { workspace = true, optional = true } diff --git a/agent_issuance/src/credential/aggregate.rs b/agent_issuance/src/credential/aggregate.rs index 652b896d..0527a4b9 100644 --- a/agent_issuance/src/credential/aggregate.rs +++ b/agent_issuance/src/credential/aggregate.rs @@ -473,7 +473,7 @@ pub mod test_utils { "id": "http://example.com/credentials/3527", "type": ["VerifiableCredential", "OpenBadgeCredential"], "issuer": { - "id": "https://my-domain.example.org", + "id": "https://my-domain.example.org/", "type": "Profile", "name": "UniCore" }, diff --git a/agent_secret_manager/Cargo.toml b/agent_secret_manager/Cargo.toml index 4c3f37be..3830af2b 100644 --- a/agent_secret_manager/Cargo.toml +++ b/agent_secret_manager/Cargo.toml @@ -10,7 +10,6 @@ agent_shared = { path = "../agent_shared" } anyhow = "1.0" async-trait = "0.1" base64.workspace = true -cqrs-es = "0.4.2" did_manager.workspace = true futures.workspace = true identity_iota.workspace = true @@ -18,7 +17,6 @@ jsonwebtoken = "9.3" log = "0.4" oid4vc-core.workspace = true p256 = { version = "0.13", features = ["jwk"] } -serde.workspace = true serde_json = "1.0" tokio.workspace = true url.workspace = true diff --git a/agent_shared/Cargo.toml b/agent_shared/Cargo.toml index d018d2d1..2ff67319 100644 --- a/agent_shared/Cargo.toml +++ b/agent_shared/Cargo.toml @@ -6,21 +6,12 @@ rust-version.workspace = true [dependencies] async-trait.workspace = true -base64.workspace = true config = { version = "0.14" } cqrs-es.workspace = true -did_manager.workspace = true dotenvy = { version = "0.15" } -identity_core = { version = "1.3" } -identity_credential.workspace = true -identity_did = { version = "1.3" } -identity_document = { version = "1.3" } -identity_storage = { version = "1.3" } -identity_verification.workspace = true -is_empty = "0.2" -jsonwebtoken.workspace = true # TODO: replace all identity_* with identity_iota? identity_iota.workspace = true +jsonwebtoken.workspace = true oid4vc-core.workspace = true oid4vci.workspace = true oid4vp.workspace = true @@ -29,7 +20,6 @@ rand = "0.8" serde.workspace = true serde_json.workspace = true serde_with = "3.0" -serde_yaml.workspace = true strum = { version = "0.26", features = ["derive"] } thiserror.workspace = true time = { version = "0.3" } From 5acfef1513106bb8b89f9a6819e41cba9b9f4851 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Wed, 18 Sep 2024 20:10:57 +0200 Subject: [PATCH 41/68] feat: add `UnsupportedCredentialFormatError` error --- agent_holder/src/offer/aggregate.rs | 2 +- agent_holder/src/offer/error.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/agent_holder/src/offer/aggregate.rs b/agent_holder/src/offer/aggregate.rs index 0debd371..e270a03c 100644 --- a/agent_holder/src/offer/aggregate.rs +++ b/agent_holder/src/offer/aggregate.rs @@ -193,7 +193,7 @@ impl Aggregate for Offer { let credential = match credential_response.credential { CredentialResponseType::Immediate { credential, .. } => { - Jwt::from(credential.as_str().unwrap().to_string()) + Jwt::from(credential.as_str().ok_or(UnsupportedCredentialFormatError)?.to_string()) } CredentialResponseType::Deferred { .. } => { return Err(UnsupportedDeferredCredentialResponseError) diff --git a/agent_holder/src/offer/error.rs b/agent_holder/src/offer/error.rs index eabbfdd3..51858c1a 100644 --- a/agent_holder/src/offer/error.rs +++ b/agent_holder/src/offer/error.rs @@ -32,4 +32,6 @@ pub enum OfferError { UnsupportedDeferredCredentialResponseError, #[error("Batch Credential Request are not supported")] BatchCredentialRequestError, + #[error("Non-JWT credentials are not supported")] + UnsupportedCredentialFormatError, } From afefa880fe3698ba5c9c6ed3a9d673105078e37c Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Thu, 19 Sep 2024 17:05:35 +0200 Subject: [PATCH 42/68] test: update Postman Collection --- .../postman/ssi-agent.postman_collection.json | 182 ++++++++++++++++-- 1 file changed, 166 insertions(+), 16 deletions(-) diff --git a/agent_api_rest/postman/ssi-agent.postman_collection.json b/agent_api_rest/postman/ssi-agent.postman_collection.json index 924b87ad..226f35c0 100644 --- a/agent_api_rest/postman/ssi-agent.postman_collection.json +++ b/agent_api_rest/postman/ssi-agent.postman_collection.json @@ -26,6 +26,16 @@ "type": "text/javascript", "packages": {} } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } } ], "request": { @@ -33,7 +43,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"offerId\":\"{{OFFER_ID}}\",\n \"credentialConfigurationId\": \"openbadge_credential\",\n \"credential\": {\n \"credentialSubject\": {\n \"type\": [ \"AchievementSubject\" ],\n \"achievement\": {\n \"id\": \"https://example.com/achievements/21st-century-skills/teamwork\",\n \"type\": \"Achievement\",\n \"criteria\": {\n \"narrative\": \"Team members are nominated for this badge by their peers and recognized upon review by Example Corp management.\"\n },\n \"description\": \"This badge recognizes the development of the capacity to collaborate within a group environment.\",\n \"name\": \"Teamwork\"\n }\n }\n }\n}", + "raw": "{\n \"offerId\": {{OFFER_ID}},\n \"credentialConfigurationId\": \"openbadge_credential\",\n \"credential\":{\n \"credentialSubject\":{\n \"type\":[\"AchievementSubject\"],\n \"achievement\": {\n \"id\": \"https://demo.edubadges.nl/public/assertions/DAO4oUapQ_eJr9VwMz6jIQ\",\n \"type\": \"Achievement\",\n \"criteria\":{\"narrative\": \"testtesttesttesttesttesttest\"},\n \"description\": \"testtesttesttesttesttesttesttest\",\n \"name\": \"Geschiedenis van de Oudheid\",\n \"image\":{\n \"id\": \"https://api-demo.edubadges.nl/media/uploads/badges/issuer_badgeclass_3e51ca72-ee9b-493b-b787-fd89f2df3189.png\",\n \"type\": \"Image\"\n }\n }\n }\n }\n }", "options": { "raw": { "language": "json" @@ -74,8 +84,11 @@ { "listen": "prerequest", "script": { - "exec": [], - "type": "text/javascript" + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} } } ], @@ -104,6 +117,60 @@ }, "response": [] }, + { + "name": "temp_thuiswinkel_credential", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const location = pm.response.headers.get(\"LOCATION\");", + "", + "if(location){", + " pm.collectionVariables.set(\"CREDENTIAL_LOCATION\",location)", + "}", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"offerId\":\"{{OFFER_ID}}\",\n \"credentialConfigurationId\": \"w3c_vc_credential\",\n \"credential\": {\n \"credentialSubject\": {\n \"id\": \"https://ecommerce.impierce.com/\",\n \"image\": \"https://www.thuiswinkel.org/Images/logo-thuiswinkel_waarborg.svg\",\n \"name\": \"Impierce Bar\",\n \"certificaat\": {\n \"type\": \"ThuiswinkelWaarborg\",\n \"certificeringsDatum\": \"2024-06-26\",\n \"geldigheidsPeriode\": \"1 jaar\",\n \"garanties\": [\n \"Het bedrijf is echt en bereikbaar.\",\n \"Voldoet aan de Thuiswinkel Algemene Voorwaarden.\",\n \"14 dagen bedenktijd.\",\n \"Veilige betaalmethoden.\",\n \"Duidelijke product/servicebeschrijvingen.\",\n \"Transparant bestelproces.\",\n \"Duidelijke prijzen.\",\n \"Veilige betaalomgeving.\",\n \"Veilige omgang met persoonlijke gegevens.\",\n \"Effectieve klachtenafhandeling en onafhankelijke geschillenbemiddeling.\"\n ]\n }\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{HOST}}/v0/credentials", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "credentials" + ] + } + }, + "response": [] + }, { "name": "credentials", "request": { @@ -649,8 +716,11 @@ { "listen": "prerequest", "script": { - "exec": [], - "type": "text/javascript" + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} } } ], @@ -671,6 +741,48 @@ }, "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": [] + }, { "name": "credentials", "request": { @@ -691,42 +803,80 @@ "response": [] }, { - "name": "offers_accept", + "name": "presentations", "request": { - "method": "POST", + "method": "GET", "header": [], "url": { - "raw": "{{HOST}}/v0/holder/offers/{{RECEIVED_OFFER_ID}}/accept", + "raw": "{{HOST}}/v0/holder/presentations", "host": [ "{{HOST}}" ], "path": [ "v0", "holder", - "offers", - "{{RECEIVED_OFFER_ID}}", - "accept" + "presentations" ] } }, "response": [] }, { - "name": "offers_reject", + "name": "presentations", "request": { "method": "POST", "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"credentialIds\": [\"credential_id\"]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { - "raw": "{{HOST}}/v0/holder/offers/{{RECEIVED_OFFER_ID}}/reject", + "raw": "{{HOST}}/v0/holder/presentations", "host": [ "{{HOST}}" ], "path": [ "v0", "holder", - "offers", - "{{RECEIVED_OFFER_ID}}", - "reject" + "presentations" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Identity", + "item": [ + { + "name": "services/linked-vp", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"presentationId\": \"presentation_id\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{HOST}}/v0/services/linked-vp", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "services", + "linked-vp" ] } }, From 3649e630213e710736b6bf6d8ec98202ebc7c6f0 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Thu, 19 Sep 2024 17:28:28 +0200 Subject: [PATCH 43/68] feat: update `init.sql` file --- agent_application/docker/db/init.sql | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/agent_application/docker/db/init.sql b/agent_application/docker/db/init.sql index f333905c..b7926fbb 100644 --- a/agent_application/docker/db/init.sql +++ b/agent_application/docker/db/init.sql @@ -10,6 +10,22 @@ CREATE TABLE events PRIMARY KEY (aggregate_type, aggregate_id, sequence) ); +CREATE TABLE document +( + view_id text NOT NULL, + version bigint CHECK (version >= 0) NOT NULL, + payload json NOT NULL, + PRIMARY KEY (view_id) +); + +CREATE TABLE service +( + view_id text NOT NULL, + version bigint CHECK (version >= 0) NOT NULL, + payload json NOT NULL, + PRIMARY KEY (view_id) +); + CREATE TABLE offer ( view_id text NOT NULL, @@ -83,6 +99,23 @@ CREATE TABLE all_credentials PRIMARY KEY (view_id) ); +CREATE TABLE presentation +( + view_id text NOT NULL, + version bigint CHECK (version >= 0) NOT NULL, + payload json NOT NULL, + PRIMARY KEY (view_id) +); + + +CREATE TABLE all_presentations +( + 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 d9360872b491db580967d80b499937df5db6110c Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Thu, 19 Sep 2024 17:29:04 +0200 Subject: [PATCH 44/68] feat: add tests and error handling --- Cargo.lock | 19 +++ agent_holder/src/presentation/aggregate.rs | 2 - agent_identity/Cargo.toml | 34 +++++- agent_identity/src/document/aggregate.rs | 131 +++++++++++++++++++-- agent_identity/src/document/error.rs | 9 +- agent_identity/src/service/aggregate.rs | 114 +++++++++++++++--- agent_identity/src/services.rs | 17 ++- agent_identity/src/state.rs | 2 +- 8 files changed, 299 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b7d4b597..3f955f93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,21 +184,40 @@ dependencies = [ name = "agent_identity" version = "0.1.0" dependencies = [ + "agent_api_rest", + "agent_holder", + "agent_identity", + "agent_issuance", "agent_secret_manager", "agent_shared", + "agent_store", + "async-std", "async-trait", + "axum 0.7.5", "base64 0.22.1", "cqrs-es", "derivative", "did_manager", + "futures", "identity_core", "identity_credential", "identity_document", "jsonwebtoken", + "lazy_static", + "mime", + "names", + "oid4vc-core", + "rand 0.8.5", + "reqwest 0.12.5", + "rstest", "serde", "serde_json", + "serial_test", "thiserror", + "tokio", + "tower", "tracing", + "tracing-test", ] [[package]] diff --git a/agent_holder/src/presentation/aggregate.rs b/agent_holder/src/presentation/aggregate.rs index bc5b3760..fd95f122 100644 --- a/agent_holder/src/presentation/aggregate.rs +++ b/agent_holder/src/presentation/aggregate.rs @@ -12,9 +12,7 @@ use tracing::info; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Presentation { - // FIX THISS pub presentation_id: String, - // pub credential_ids: Vec, pub signed: Option, } diff --git a/agent_identity/Cargo.toml b/agent_identity/Cargo.toml index a16f539f..6794bd0a 100644 --- a/agent_identity/Cargo.toml +++ b/agent_identity/Cargo.toml @@ -17,10 +17,42 @@ identity_credential.workspace = true identity_core = "1.3" identity_document = { version = "1.3" } jsonwebtoken.workspace = true +oid4vc-core.workspace = true serde.workspace = true serde_json.workspace = true thiserror.workspace = true tracing.workspace = true +# `test_utils` dependencies +futures = { workspace = true, optional = true } +rstest = { workspace = true, optional = true } +tokio = { workspace = true, optional = true } + +[dev-dependencies] +agent_api_rest = { path = "../agent_api_rest" } +agent_holder = { path = "../agent_holder", features = ["test_utils"] } +agent_identity = { 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" } + +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"] } + [features] -test_utils = [] +test_utils = [ + "dep:futures", + "dep:rstest", + "dep:tokio", +] diff --git a/agent_identity/src/document/aggregate.rs b/agent_identity/src/document/aggregate.rs index 1313f438..efeda503 100644 --- a/agent_identity/src/document/aggregate.rs +++ b/agent_identity/src/document/aggregate.rs @@ -51,21 +51,21 @@ impl Aggregate for Document { .produce_document( did_method, method_specific_parameters, - // TODO: This way the Document can only support on single algorithm. We need to support multiple algorithms. + // TODO: This way the Document can only support on single algorithm. We need to make sure that + // Documents can support multiple algorithms. from_jsonwebtoken_algorithm_to_jwsalgorithm(&get_preferred_signing_algorithm()), ) .await - // FIX THISS - .unwrap(); + .map_err(|err| ProduceDocumentError(err.to_string()))?; Ok(vec![DocumentCreated { document }]) } AddService { service } => { - // FIX THIS - let mut document = self.document.clone().unwrap(); + let mut document = self.document.clone().ok_or(MissingDocumentError)?; - // FIX THIS - document.insert_service(service).unwrap(); + document + .insert_service(service) + .map_err(|err| AddServiceError(err.to_string()))?; Ok(vec![ServiceAdded { document }]) } @@ -87,3 +87,120 @@ impl Aggregate for Document { } } } + +#[cfg(test)] +pub mod document_tests { + use super::test_utils::*; + use super::*; + use cqrs_es::test::TestFramework; + use identity_document::service::Service; + use rstest::rstest; + + type DocumentTestFramework = TestFramework; + + #[rstest] + #[serial_test::serial] + async fn test_create_document(did_method: DidMethod, #[future(awt)] document: CoreDocument) { + DocumentTestFramework::with(IdentityServices::default()) + .given_no_previous_events() + .when(DocumentCommand::CreateDocument { did_method }) + .then_expect_events(vec![DocumentEvent::DocumentCreated { document }]) + } + + #[rstest] + #[serial_test::serial] + async fn test_add_service( + #[future(awt)] document: CoreDocument, + domain_linkage_service: Service, + #[future(awt)] document_with_domain_linkage_service: CoreDocument, + ) { + DocumentTestFramework::with(IdentityServices::default()) + .given(vec![DocumentEvent::DocumentCreated { document }]) + .when(DocumentCommand::AddService { + service: domain_linkage_service, + }) + .then_expect_events(vec![DocumentEvent::ServiceAdded { + document: document_with_domain_linkage_service, + }]) + } +} + +#[cfg(feature = "test_utils")] +pub mod test_utils { + use agent_secret_manager::secret_manager; + use agent_shared::{ + config::{config, get_preferred_signing_algorithm}, + from_jsonwebtoken_algorithm_to_jwsalgorithm, + }; + use did_manager::{DidMethod, MethodSpecificParameters}; + use identity_core::convert::FromJson; + use identity_document::{ + document::CoreDocument, + service::{Service, ServiceEndpoint}, + }; + use rstest::*; + use serde_json::json; + + #[fixture] + pub fn did_method() -> DidMethod { + DidMethod::Web + } + + #[fixture] + pub async fn document(did_method: DidMethod) -> CoreDocument { + let mut secret_manager = secret_manager().await; + + let method_specific_parameters = matches!(did_method, DidMethod::Web).then(|| MethodSpecificParameters::Web { + origin: config().url.origin(), + }); + + secret_manager + .produce_document( + did_method, + method_specific_parameters, + from_jsonwebtoken_algorithm_to_jwsalgorithm(&get_preferred_signing_algorithm()), + ) + .await + .unwrap() + } + + #[fixture] + pub fn domain_linkage_service() -> Service { + Service::builder(Default::default()) + .id(format!("did:test:123#linked_domain-service").parse().unwrap()) + .type_("LinkedDomains") + .service_endpoint( + ServiceEndpoint::from_json_value(json!({ + "origins": [config().url], + })) + .unwrap(), + ) + .build() + .unwrap() + } + + #[fixture] + pub async fn document_with_domain_linkage_service( + did_method: DidMethod, + domain_linkage_service: Service, + ) -> CoreDocument { + let mut secret_manager = secret_manager().await; + + let method_specific_parameters = matches!(did_method, DidMethod::Web).then(|| MethodSpecificParameters::Web { + origin: config().url.origin(), + }); + + let mut document = secret_manager + .produce_document( + did_method, + method_specific_parameters, + from_jsonwebtoken_algorithm_to_jwsalgorithm(&get_preferred_signing_algorithm()), + ) + .await + .unwrap(); + + document.insert_service(domain_linkage_service).unwrap(); + + document + } +} diff --git a/agent_identity/src/document/error.rs b/agent_identity/src/document/error.rs index 7311b89f..21a11515 100644 --- a/agent_identity/src/document/error.rs +++ b/agent_identity/src/document/error.rs @@ -1,4 +1,11 @@ use thiserror::Error; #[derive(Error, Debug)] -pub enum DocumentError {} +pub enum DocumentError { + #[error("Error while producing DID document: {0}")] + ProduceDocumentError(String), + #[error("Missing document")] + MissingDocumentError, + #[error("Error while adding service: {0}")] + AddServiceError(String), +} diff --git a/agent_identity/src/service/aggregate.rs b/agent_identity/src/service/aggregate.rs index b1b4bf02..a9b54cdb 100644 --- a/agent_identity/src/service/aggregate.rs +++ b/agent_identity/src/service/aggregate.rs @@ -1,7 +1,7 @@ use super::{command::ServiceCommand, error::ServiceError, event::ServiceEvent}; use crate::services::IdentityServices; use agent_shared::{ - config::{config, get_preferred_signing_algorithm}, + config::{config, get_preferred_did_method, get_preferred_signing_algorithm}, from_jsonwebtoken_algorithm_to_jwsalgorithm, }; use async_trait::async_trait; @@ -18,6 +18,7 @@ use identity_credential::{ }; use identity_document::service::{Service as DocumentService, ServiceEndpoint}; use jsonwebtoken::Header; +use oid4vc_core::authentication::subject::Subject as _; use serde::{Deserialize, Serialize}; use serde_json::json; use std::sync::Arc; @@ -55,31 +56,24 @@ impl Aggregate for Service { match command { CreateDomainLinkageService { service_id } => { - let mut secret_manager = services.subject.secret_manager.lock().await; + let subject = &services.subject; let origin = config().url.origin(); - let method_specific_parameters = MethodSpecificParameters::Web { origin: origin.clone() }; - // TODO: implement for all non-deterministic methods and not just DID WEB - let document = secret_manager - .produce_document( - DidMethod::Web, - Some(method_specific_parameters), - // TODO: This way the Document can only support on single algorithm. We need to support multiple algorithms. - from_jsonwebtoken_algorithm_to_jwsalgorithm(&get_preferred_signing_algorithm()), + let subject_did = subject + .identifier( + &get_preferred_did_method().to_string(), + get_preferred_signing_algorithm(), ) .await - // FIX THISS .unwrap(); - let subject_did = document.id(); - let origin = identity_core::common::Url::parse(origin.ascii_serialization()).unwrap(); let domain_linkage_credential = DomainLinkageCredentialBuilder::new() - .issuer(subject_did.clone()) + .issuer(subject_did.parse().unwrap()) .origin(origin.clone()) .issuance_date(Timestamp::now_utc()) - // Expires after a year. + // TODO: make this configurable .expiration_date(Timestamp::now_utc().checked_add(Duration::days(365)).unwrap()) .build() // FIX THISS @@ -91,7 +85,7 @@ impl Aggregate for Service { // Compose JWT let header = Header { alg: get_preferred_signing_algorithm(), - typ: Some("JWT".to_string()), + typ: None, // TODO: make dynamic kid: Some(format!("{subject_did}#key-0")), ..Default::default() @@ -103,6 +97,8 @@ impl Aggregate for Service { ] .join("."); + let secret_manager = subject.secret_manager.lock().await; + let proof_value = secret_manager .sign( message.as_bytes(), @@ -198,3 +194,89 @@ impl Aggregate for Service { } } } + +#[cfg(test)] +pub mod service_tests { + use identity_document::service::Service as DocumentService; + + use super::test_utils::*; + use super::*; + use cqrs_es::test::TestFramework; + use rstest::rstest; + + type ServiceTestFramework = TestFramework; + + #[rstest] + #[serial_test::serial] + async fn test_create_domain_linkage_service( + domain_linkage_service_id: String, + domain_linkage_service: DocumentService, + domain_linkage_resource: ServiceResource, + ) { + ServiceTestFramework::with(IdentityServices::default()) + .given_no_previous_events() + .when(ServiceCommand::CreateDomainLinkageService { + service_id: domain_linkage_service_id.clone(), + }) + .then_expect_events(vec![ServiceEvent::DomainLinkageServiceCreated { + service_id: domain_linkage_service_id, + service: domain_linkage_service, + resource: domain_linkage_resource, + }]) + } + + // #[rstest] + // #[serial_test::serial] + // async fn test_add_service( + // #[future(awt)] document: CoreDocument, + // domain_linkage_service: Service, + // #[future(awt)] document_with_domain_linkage_service: CoreDocument, + // ) { + // DocumentTestFramework::with(IdentityServices::default()) + // .given(vec![DocumentEvent::DocumentCreated { document }]) + // .when(DocumentCommand::AddService { + // service: domain_linkage_service, + // }) + // .then_expect_events(vec![DocumentEvent::ServiceAdded { + // document: document_with_domain_linkage_service, + // }]) + // } +} + +#[cfg(feature = "test_utils")] +pub mod test_utils { + use super::*; + use crate::state::DOMAIN_LINKAGE_SERVICE_ID; + use agent_shared::config::config; + use identity_core::convert::FromJson; + use identity_document::service::{Service, ServiceEndpoint}; + use rstest::*; + use serde_json::json; + + #[fixture] + pub fn domain_linkage_service_id() -> String { + DOMAIN_LINKAGE_SERVICE_ID.to_string() + } + + #[fixture] + pub fn domain_linkage_service() -> DocumentService { + Service::builder(Default::default()) + .id(format!("did:test:123#linked_domain-service").parse().unwrap()) + .type_("LinkedDomains") + .service_endpoint( + ServiceEndpoint::from_json_value(json!({ + "origins": [config().url], + })) + .unwrap(), + ) + .build() + .unwrap() + } + + #[fixture] + pub fn domain_linkage_resource() -> ServiceResource { + let domain_linkage_configuration = DomainLinkageConfiguration::new(vec![Jwt::from("message".to_string())]); + + ServiceResource::DomainLinkage(domain_linkage_configuration) + } +} diff --git a/agent_identity/src/services.rs b/agent_identity/src/services.rs index c6602927..abb9d7f2 100644 --- a/agent_identity/src/services.rs +++ b/agent_identity/src/services.rs @@ -1,7 +1,7 @@ use agent_secret_manager::subject::Subject; use std::sync::Arc; -/// Identity services. This struct is used to sign credentials and validate credential requests. +/// Identity services. pub struct IdentityServices { pub subject: Arc, } @@ -10,4 +10,19 @@ impl IdentityServices { pub fn new(subject: Arc) -> Self { Self { subject } } + + #[cfg(feature = "test_utils")] + #[allow(clippy::should_implement_trait)] + pub fn default() -> Arc + where + Self: Sized, + { + use agent_secret_manager::secret_manager; + + Arc::new(Self::new(Arc::new(futures::executor::block_on(async { + Subject { + secret_manager: Arc::new(tokio::sync::Mutex::new(secret_manager().await)), + } + })))) + } } diff --git a/agent_identity/src/state.rs b/agent_identity/src/state.rs index dca8c19b..a16f67c6 100644 --- a/agent_identity/src/state.rs +++ b/agent_identity/src/state.rs @@ -89,7 +89,7 @@ pub async fn initialize(state: &IdentityState) { warn!("Failed to create domain linkage service"); } - let linked_domains_service = match query_handler(&DOMAIN_LINKAGE_SERVICE_ID, &state.query.service).await { + let linked_domains_service = match query_handler(DOMAIN_LINKAGE_SERVICE_ID, &state.query.service).await { Ok(Some(Service { service: Some(linked_domains_service), .. From 5ef35b6ebc06300de6cc0b0944f1d0de6ea33612 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Thu, 3 Oct 2024 22:23:38 +0200 Subject: [PATCH 45/68] feat: add error handling --- Cargo.lock | 2 + Cargo.toml | 1 + agent_api_rest/src/identity/services/mod.rs | 6 +- agent_holder/Cargo.toml | 1 + agent_holder/src/presentation/aggregate.rs | 22 +++++-- agent_holder/src/presentation/error.rs | 13 +++- agent_identity/Cargo.toml | 3 +- agent_identity/src/document/aggregate.rs | 4 +- agent_identity/src/service/aggregate.rs | 73 +++++++++++++-------- agent_identity/src/service/command.rs | 2 +- agent_identity/src/service/error.rs | 21 +++++- agent_issuance/Cargo.toml | 2 +- agent_verification/Cargo.toml | 2 +- 13 files changed, 109 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3f955f93..6afb8a94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,6 +160,7 @@ dependencies = [ "base64 0.22.1", "cqrs-es", "did_manager", + "identity_core", "identity_credential", "jsonwebtoken", "lazy_static", @@ -201,6 +202,7 @@ dependencies = [ "futures", "identity_core", "identity_credential", + "identity_did", "identity_document", "jsonwebtoken", "lazy_static", diff --git a/Cargo.toml b/Cargo.toml index 318a16b7..17bc2ae4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ axum = { version = "0.7", features = ["tracing"] } base64 = "0.22" cqrs-es = "0.4.2" futures = "0.3" +identity_core = "1.3" identity_credential = { version = "1.3", default-features = false, features = [ "validator", "credential", diff --git a/agent_api_rest/src/identity/services/mod.rs b/agent_api_rest/src/identity/services/mod.rs index 04d5985d..cee2f425 100644 --- a/agent_api_rest/src/identity/services/mod.rs +++ b/agent_api_rest/src/identity/services/mod.rs @@ -18,21 +18,21 @@ use tracing::info; #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct LinkedVPEndpointRequest { - pub presentation_id: String, + pub presentation_ids: Vec, } #[axum_macros::debug_handler] pub(crate) async fn linked_vp(State(state): State, Json(payload): Json) -> Response { info!("Request Body: {}", payload); - let Ok(LinkedVPEndpointRequest { presentation_id }) = serde_json::from_value(payload) else { + let Ok(LinkedVPEndpointRequest { presentation_ids }) = serde_json::from_value(payload) else { return (StatusCode::BAD_REQUEST, "invalid payload").into_response(); }; let service_id = "linked-verifiable-presentation-service".to_string(); let command = ServiceCommand::CreateLinkedVerifiablePresentationService { service_id: service_id.clone(), - presentation_id, + presentation_ids, }; // Create a linked verifiable presentation service. diff --git a/agent_holder/Cargo.toml b/agent_holder/Cargo.toml index f079e626..b7b9e004 100644 --- a/agent_holder/Cargo.toml +++ b/agent_holder/Cargo.toml @@ -11,6 +11,7 @@ agent_secret_manager = { path = "../agent_secret_manager" } async-trait.workspace = true base64.workspace = true cqrs-es.workspace = true +identity_core.workspace = true identity_credential.workspace = true jsonwebtoken.workspace = true oid4vci.workspace = true diff --git a/agent_holder/src/presentation/aggregate.rs b/agent_holder/src/presentation/aggregate.rs index fd95f122..68c92b7d 100644 --- a/agent_holder/src/presentation/aggregate.rs +++ b/agent_holder/src/presentation/aggregate.rs @@ -4,6 +4,7 @@ use agent_shared::config::{get_preferred_did_method, get_preferred_signing_algor use async_trait::async_trait; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use cqrs_es::Aggregate; +use identity_core::convert::ToJson; use identity_credential::credential::Jwt; use jsonwebtoken::{Algorithm, Header}; use serde::{Deserialize, Serialize}; @@ -46,10 +47,12 @@ impl Aggregate for Presentation { get_preferred_signing_algorithm(), ) .await - .unwrap(); + .map_err(|err| MissingIdentifierError(err.to_string()))?; let mut presentation_builder = identity_credential::presentation::Presentation::builder( - subject_did.parse().unwrap(), + subject_did + .parse::() + .map_err(|err| InvalidUrlError(err.to_string()))?, Default::default(), ); for signed_credential in signed_credentials { @@ -57,11 +60,13 @@ impl Aggregate for Presentation { } let verifiable_presentation: identity_credential::presentation::Presentation = - presentation_builder.build().unwrap(); + presentation_builder + .build() + .map_err(|err| PresentationBuilderError(err.to_string()))?; let payload = verifiable_presentation .serialize_jwt(&Default::default()) - .expect("FIX THISS"); + .map_err(|err| SerializationError(err.to_string()))?; // Compose JWT let header = Header { @@ -72,8 +77,11 @@ impl Aggregate for Presentation { }; let message = [ - URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap().as_slice()), - // FIX THISS? + URL_SAFE_NO_PAD.encode( + header + .to_json_vec() + .map_err(|err| SerializationError(err.to_string()))?, + ), URL_SAFE_NO_PAD.encode(payload.as_bytes()), ] .join("."); @@ -85,7 +93,7 @@ impl Aggregate for Presentation { get_preferred_signing_algorithm(), ) .await - .unwrap(); + .map_err(|err| SigningError(err.to_string()))?; let signature = URL_SAFE_NO_PAD.encode(proof_value.as_slice()); let message = [message, signature].join("."); diff --git a/agent_holder/src/presentation/error.rs b/agent_holder/src/presentation/error.rs index fa861056..c76dca36 100644 --- a/agent_holder/src/presentation/error.rs +++ b/agent_holder/src/presentation/error.rs @@ -1,4 +1,15 @@ use thiserror::Error; #[derive(Error, Debug)] -pub enum PresentationError {} +pub enum PresentationError { + #[error("Failed to serialize presentation: {0}")] + SerializationError(String), + #[error("Failed to build presentation: {0}")] + PresentationBuilderError(String), + #[error("Invalid URL: {0}")] + InvalidUrlError(String), + #[error("Missing identifier: {0}")] + MissingIdentifierError(String), + #[error("Failed to sign presentation: {0}")] + SigningError(String), +} diff --git a/agent_identity/Cargo.toml b/agent_identity/Cargo.toml index 6794bd0a..2436e921 100644 --- a/agent_identity/Cargo.toml +++ b/agent_identity/Cargo.toml @@ -14,7 +14,8 @@ cqrs-es.workspace = true derivative = "2.2" did_manager.workspace = true identity_credential.workspace = true -identity_core = "1.3" +identity_core.workspace = true +identity_did = { version = "1.3" } identity_document = { version = "1.3" } jsonwebtoken.workspace = true oid4vc-core.workspace = true diff --git a/agent_identity/src/document/aggregate.rs b/agent_identity/src/document/aggregate.rs index efeda503..c8f3e06d 100644 --- a/agent_identity/src/document/aggregate.rs +++ b/agent_identity/src/document/aggregate.rs @@ -63,6 +63,8 @@ impl Aggregate for Document { AddService { service } => { let mut document = self.document.clone().ok_or(MissingDocumentError)?; + // Overwrite the service if it already exists. + document.remove_service(service.id()); document .insert_service(service) .map_err(|err| AddServiceError(err.to_string()))?; @@ -167,7 +169,7 @@ pub mod test_utils { #[fixture] pub fn domain_linkage_service() -> Service { Service::builder(Default::default()) - .id(format!("did:test:123#linked_domain-service").parse().unwrap()) + .id("did:test:123#linked_domain-service".parse().unwrap()) .type_("LinkedDomains") .service_endpoint( ServiceEndpoint::from_json_value(json!({ diff --git a/agent_identity/src/service/aggregate.rs b/agent_identity/src/service/aggregate.rs index a9b54cdb..b83250a2 100644 --- a/agent_identity/src/service/aggregate.rs +++ b/agent_identity/src/service/aggregate.rs @@ -9,13 +9,14 @@ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use cqrs_es::Aggregate; use did_manager::{DidMethod, MethodSpecificParameters}; use identity_core::{ - common::{Duration, Timestamp}, - convert::FromJson, + common::{Duration, OrderedSet, Timestamp}, + convert::{FromJson, ToJson}, }; use identity_credential::{ credential::Jwt, domain_linkage::{DomainLinkageConfiguration, DomainLinkageCredentialBuilder}, }; +use identity_did::{CoreDID, DIDUrl}; use identity_document::service::{Service as DocumentService, ServiceEndpoint}; use jsonwebtoken::Header; use oid4vc_core::authentication::subject::Subject as _; @@ -66,21 +67,28 @@ impl Aggregate for Service { get_preferred_signing_algorithm(), ) .await - .unwrap(); + .map_err(|err| MissingIdentifierError(err.to_string()))?; - let origin = identity_core::common::Url::parse(origin.ascii_serialization()).unwrap(); + let origin = identity_core::common::Url::parse(origin.ascii_serialization()) + .map_err(|err| InvalidUrlError(err.to_string()))?; let domain_linkage_credential = DomainLinkageCredentialBuilder::new() - .issuer(subject_did.parse().unwrap()) + .issuer( + subject_did + .parse::() + .map_err(|err| InvalidDidError(err.to_string()))?, + ) .origin(origin.clone()) .issuance_date(Timestamp::now_utc()) // TODO: make this configurable - .expiration_date(Timestamp::now_utc().checked_add(Duration::days(365)).unwrap()) + .expiration_date( + Timestamp::now_utc() + .checked_add(Duration::days(365)) + .ok_or(InvalidTimestampError)?, + ) .build() - // FIX THISS - .unwrap() + .map_err(|err| DomainLinkageCredentialBuilderError(err.to_string()))? .serialize_jwt(Default::default()) - // FIX THISS - .unwrap(); + .map_err(|err| SerializationError(err.to_string()))?; // Compose JWT let header = Header { @@ -92,7 +100,11 @@ impl Aggregate for Service { }; let message = [ - URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap().as_slice()), + URL_SAFE_NO_PAD.encode( + header + .to_json_vec() + .map_err(|err| SerializationError(err.to_string()))?, + ), URL_SAFE_NO_PAD.encode(domain_linkage_credential.as_bytes()), ] .join("."); @@ -105,7 +117,7 @@ impl Aggregate for Service { from_jsonwebtoken_algorithm_to_jwsalgorithm(&get_preferred_signing_algorithm()), ) .await - .unwrap(); + .map_err(|err| SigningError(err.to_string()))?; let signature = URL_SAFE_NO_PAD.encode(proof_value.as_slice()); let message = [message, signature].join("."); @@ -114,13 +126,15 @@ impl Aggregate for Service { // Create a new service and add it to the DID document. let service = DocumentService::builder(Default::default()) - .id(format!("{subject_did}#{service_id}").parse().unwrap()) + .id(format!("{subject_did}#{service_id}") + .parse::() + .map_err(|err| InvalidUrlError(err.to_string()))?) .type_("LinkedDomains") .service_endpoint( ServiceEndpoint::from_json_value(json!({ "origins": [origin] })) - .unwrap(), + .map_err(|err| InvalidServiceEndpointError(err.to_string()))?, ) .build() .expect("Failed to create DID Configuration Resource"); @@ -133,13 +147,14 @@ impl Aggregate for Service { } CreateLinkedVerifiablePresentationService { service_id, - presentation_id, + presentation_ids, } => { let mut secret_manager = services.subject.secret_manager.lock().await; let origin = config().url.origin(); let method_specific_parameters = MethodSpecificParameters::Web { origin: origin.clone() }; - let origin = identity_core::common::Url::parse(origin.ascii_serialization()).unwrap(); + let origin = identity_core::common::Url::parse(origin.ascii_serialization()) + .map_err(|err| InvalidUrlError(err.to_string()))?; // TODO: implement for all non-deterministic methods and not just DID WEB let document = secret_manager @@ -150,20 +165,26 @@ impl Aggregate for Service { from_jsonwebtoken_algorithm_to_jwsalgorithm(&get_preferred_signing_algorithm()), ) .await - // FIX THISS - .unwrap(); + .map_err(|err| ProduceDocumentError(err.to_string()))?; let subject_did = document.id(); let service = DocumentService::builder(Default::default()) - .id(format!("{subject_did}#{service_id}").parse().unwrap()) + .id(format!("{subject_did}#{service_id}") + .parse::() + .map_err(|err| InvalidUrlError(err.to_string()))?) .type_("LinkedVerifiablePresentation") - .service_endpoint(ServiceEndpoint::from( - // FIX THIS - format!("{origin}v0/holder/presentations/{presentation_id}/signed") - .parse::() - .unwrap(), - )) + .service_endpoint(ServiceEndpoint::from(OrderedSet::from_iter( + presentation_ids + .into_iter() + .map(|presentation_id| { + // TODO: Find a better way to construct the URL + format!("{origin}v0/holder/presentations/{presentation_id}/signed") + .parse::() + }) + .collect::, _>>() + .map_err(|err| InvalidUrlError(err.to_string()))?, + ))) .build() .expect("Failed to create Linked Verifiable Presentation Resource"); @@ -261,7 +282,7 @@ pub mod test_utils { #[fixture] pub fn domain_linkage_service() -> DocumentService { Service::builder(Default::default()) - .id(format!("did:test:123#linked_domain-service").parse().unwrap()) + .id("did:test:123#linked_domain-service".parse().unwrap()) .type_("LinkedDomains") .service_endpoint( ServiceEndpoint::from_json_value(json!({ diff --git a/agent_identity/src/service/command.rs b/agent_identity/src/service/command.rs index 6078b06b..5584ceea 100644 --- a/agent_identity/src/service/command.rs +++ b/agent_identity/src/service/command.rs @@ -8,6 +8,6 @@ pub enum ServiceCommand { }, CreateLinkedVerifiablePresentationService { service_id: String, - presentation_id: String, + presentation_ids: Vec, }, } diff --git a/agent_identity/src/service/error.rs b/agent_identity/src/service/error.rs index c85a9221..530bc722 100644 --- a/agent_identity/src/service/error.rs +++ b/agent_identity/src/service/error.rs @@ -1,4 +1,23 @@ use thiserror::Error; #[derive(Error, Debug)] -pub enum ServiceError {} +pub enum ServiceError { + #[error("Missing identifier: {0}")] + MissingIdentifierError(String), + #[error("Invalid URL: {0}")] + InvalidUrlError(String), + #[error("Invalid DID: {0}")] + InvalidDidError(String), + #[error("Failed to build the Domain Linkage Credential: {0}")] + DomainLinkageCredentialBuilderError(String), + #[error("Failed to serialize credential: {0}")] + SerializationError(String), + #[error("Failed to sign proof: {0}")] + SigningError(String), + #[error("Invalid timestamp")] + InvalidTimestampError, + #[error("Invalid service endpoint: {0}")] + InvalidServiceEndpointError(String), + #[error("Error producing document: {0}")] + ProduceDocumentError(String), +} diff --git a/agent_issuance/Cargo.toml b/agent_issuance/Cargo.toml index 3049eaf9..a6ba06fa 100644 --- a/agent_issuance/Cargo.toml +++ b/agent_issuance/Cargo.toml @@ -13,7 +13,7 @@ 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" -identity_core = "1.3" +identity_core.workspace = true identity_credential.workspace = true jsonwebtoken.workspace = true oid4vci.workspace = true diff --git a/agent_verification/Cargo.toml b/agent_verification/Cargo.toml index 57907114..86a092c8 100644 --- a/agent_verification/Cargo.toml +++ b/agent_verification/Cargo.toml @@ -30,7 +30,7 @@ agent_verification = { path = ".", features = ["test_utils"] } async-std = { version = "1.5", features = ["attributes", "tokio1"] } did_manager.workspace = true -identity_core = "1.2.0" +identity_core.workspace = true identity_credential.workspace = true lazy_static.workspace = true oid4vci.workspace = true From ffd17545bf1360553ff005aadd4a6dcd1939fa03 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 4 Oct 2024 14:44:21 +0200 Subject: [PATCH 46/68] test: add unit tests for `Service`, `Presentation` and received `Offer` --- agent_holder/src/offer/aggregate.rs | 11 +- agent_holder/src/presentation/aggregate.rs | 60 ++++++++++- agent_identity/src/service/aggregate.rs | 118 ++++++++++++++++----- agent_identity/src/state.rs | 3 + agent_shared/src/config.rs | 16 ++- 5 files changed, 169 insertions(+), 39 deletions(-) diff --git a/agent_holder/src/offer/aggregate.rs b/agent_holder/src/offer/aggregate.rs index e0750693..efbfce69 100644 --- a/agent_holder/src/offer/aggregate.rs +++ b/agent_holder/src/offer/aggregate.rs @@ -426,6 +426,7 @@ pub mod tests { #[future(awt)] credential_offer_parameters: Box, #[future(awt)] token_response: TokenResponse, credential_configurations_supported: HashMap, + signed_credentials: Vec, ) { OfferTestFramework::with(Service::default()) .given(vec![ @@ -440,7 +441,7 @@ pub mod tests { }, OfferEvent::TokenResponseReceived { offer_id: offer_id.clone(), - token_response + token_response, }, ]) .when_async(OfferCommand::SendCredentialRequest { @@ -450,7 +451,7 @@ pub mod tests { .then_expect_events(vec![OfferEvent::CredentialResponseReceived { offer_id: offer_id.clone(), status: Status::Received, - credentials: vec![Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJkZWdyZWUiOnsidHlwZSI6Ik1hc3RlckRlZ3JlZSIsIm5hbWUiOiJNYXN0ZXIgb2YgT2NlYW5vZ3JhcGh5In0sImZpcnN0X25hbWUiOiJGZXJyaXMiLCJsYXN0X25hbWUiOiJSdXN0YWNlYW4ifSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiJ9fQ.jQEpI7DhjOcmyhPEpfGARwcRyzor_fUvynb43-eqD9175FBoshENX0S-8qlloQ7vbT5gat8TjvcDlGDN720ZBw".to_string())], + credentials: signed_credentials, }]); } @@ -482,10 +483,16 @@ pub mod tests { #[cfg(feature = "test_utils")] pub mod test_utils { use agent_shared::generate_random_string; + use identity_credential::credential::Jwt; use rstest::*; #[fixture] pub fn offer_id() -> String { generate_random_string() } + + #[fixture] + pub fn signed_credentials() -> Vec { + vec![Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJkZWdyZWUiOnsidHlwZSI6Ik1hc3RlckRlZ3JlZSIsIm5hbWUiOiJNYXN0ZXIgb2YgT2NlYW5vZ3JhcGh5In0sImZpcnN0X25hbWUiOiJGZXJyaXMiLCJsYXN0X25hbWUiOiJSdXN0YWNlYW4ifSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiJ9fQ.jQEpI7DhjOcmyhPEpfGARwcRyzor_fUvynb43-eqD9175FBoshENX0S-8qlloQ7vbT5gat8TjvcDlGDN720ZBw".to_string())] + } } diff --git a/agent_holder/src/presentation/aggregate.rs b/agent_holder/src/presentation/aggregate.rs index 68c92b7d..456257a2 100644 --- a/agent_holder/src/presentation/aggregate.rs +++ b/agent_holder/src/presentation/aggregate.rs @@ -4,9 +4,9 @@ use agent_shared::config::{get_preferred_did_method, get_preferred_signing_algor use async_trait::async_trait; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use cqrs_es::Aggregate; -use identity_core::convert::ToJson; -use identity_credential::credential::Jwt; -use jsonwebtoken::{Algorithm, Header}; +use identity_core::{common::Timestamp, convert::ToJson}; +use identity_credential::{credential::Jwt, presentation::JwtPresentationOptions}; +use jsonwebtoken::Header; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tracing::info; @@ -59,18 +59,23 @@ impl Aggregate for Presentation { presentation_builder = presentation_builder.credential(signed_credential); } + #[cfg(feature = "test_utils")] + let options = JwtPresentationOptions::default().issuance_date(Timestamp::from_unix(0).unwrap()); + #[cfg(not(feature = "test_utils"))] + let options = JwtPresentationOptions::default(); + let verifiable_presentation: identity_credential::presentation::Presentation = presentation_builder .build() .map_err(|err| PresentationBuilderError(err.to_string()))?; let payload = verifiable_presentation - .serialize_jwt(&Default::default()) + .serialize_jwt(&options) .map_err(|err| SerializationError(err.to_string()))?; // Compose JWT let header = Header { - alg: Algorithm::ES256, + alg: get_preferred_signing_algorithm(), typ: Some("JWT".to_string()), kid: Some(format!("{subject_did}#key-0")), ..Default::default() @@ -121,3 +126,48 @@ impl Aggregate for Presentation { } } } + +#[cfg(test)] +pub mod presentation_tests { + + use crate::offer::aggregate::test_utils::signed_credentials; + + use super::test_utils::*; + use super::*; + use agent_secret_manager::service::Service; + use cqrs_es::test::TestFramework; + use rstest::rstest; + + type PresentationTestFramework = TestFramework; + + #[rstest] + #[serial_test::serial] + async fn test_create_presentation(presentation_id: String, signed_credentials: Vec, signed_presentation: Jwt) { + PresentationTestFramework::with(Service::default()) + .given_no_previous_events() + .when(PresentationCommand::CreatePresentation { + presentation_id: presentation_id.clone(), + signed_credentials, + }) + .then_expect_events(vec![PresentationEvent::PresentationCreated { + presentation_id, + signed_presentation, + }]) + } +} + +#[cfg(feature = "test_utils")] +pub mod test_utils { + use super::*; + use rstest::*; + + #[fixture] + pub fn presentation_id() -> String { + "presentation-id".to_string() + } + + #[fixture] + pub fn signed_presentation() -> Jwt { + Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I2tleS0wIn0.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsIm5iZiI6MCwidnAiOnsiQGNvbnRleHQiOiJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsInR5cGUiOiJWZXJpZmlhYmxlUHJlc2VudGF0aW9uIiwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOlsiZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKRlpFUlRRU0lzSW10cFpDSTZJbVJwWkRwclpYazZlalpOYTJkRk9EUk9RMDF3VFdWQmVEbHFTemxqWmpWWE5FYzRaMk5hT1hoMWQwcDJSekZsTjNkT2F6aExRMmQwSTNvMlRXdG5SVGcwVGtOTmNFMWxRWGc1YWtzNVkyWTFWelJIT0dkaldqbDRkWGRLZGtjeFpUZDNUbXM0UzBObmRDSjkuZXlKcGMzTWlPaUprYVdRNmEyVjVPbm8yVFd0blJUZzBUa05OY0UxbFFYZzVha3M1WTJZMVZ6UkhPR2RqV2psNGRYZEtka2N4WlRkM1RtczRTME5uZENJc0luTjFZaUk2SW1ScFpEcHJaWGs2ZWpaTmEyZEZPRFJPUTAxd1RXVkJlRGxxU3psalpqVlhORWM0WjJOYU9YaDFkMHAyUnpGbE4zZE9hemhMUTJkMElpd2laWGh3SWpvNU9UazVPVGs1T1RrNUxDSnBZWFFpT2pBc0luWmpJanA3SWtCamIyNTBaWGgwSWpvaWFIUjBjSE02THk5M2QzY3Vkek11YjNKbkx6SXdNVGd2WTNKbFpHVnVkR2xoYkhNdmRqRWlMQ0owZVhCbElqcGJJbFpsY21sbWFXRmliR1ZEY21Wa1pXNTBhV0ZzSWwwc0ltTnlaV1JsYm5ScFlXeFRkV0pxWldOMElqcDdJbWxrSWpvaVpHbGtPbXRsZVRwNk5rMXJaMFU0TkU1RFRYQk5aVUY0T1dwTE9XTm1OVmMwUnpoblkxbzVlSFYzU25aSE1XVTNkMDVyT0V0RFozUWlMQ0prWldkeVpXVWlPbnNpZEhsd1pTSTZJazFoYzNSbGNrUmxaM0psWlNJc0ltNWhiV1VpT2lKTllYTjBaWElnYjJZZ1QyTmxZVzV2WjNKaGNHaDVJbjBzSW1acGNuTjBYMjVoYldVaU9pSkdaWEp5YVhNaUxDSnNZWE4wWDI1aGJXVWlPaUpTZFhOMFlXTmxZVzRpZlN3aWFYTnpkV1Z5SWpvaVpHbGtPbXRsZVRwNk5rMXJaMFU0TkU1RFRYQk5aVUY0T1dwTE9XTm1OVmMwUnpoblkxbzVlSFYzU25aSE1XVTNkMDVyT0V0RFozUWlMQ0pwYzNOMVlXNWpaVVJoZEdVaU9pSXlNREV3TFRBeExUQXhWREF3T2pBd09qQXdXaUo5ZlEualFFcEk3RGhqT2NteWhQRXBmR0FSd2NSeXpvcl9mVXZ5bmI0My1lcUQ5MTc1RkJvc2hFTlgwUy04cWxsb1E3dmJUNWdhdDhUanZjRGxHRE43MjBaQnciXX19.2iIO7zlcLsceC5P0X3p9yICrqRXj8A9VcTVJkUUiALufEm72urbJFRbkvrXGNWwYezFzAOz-4WrGpUNHWtTDCA".to_string()) + } +} diff --git a/agent_identity/src/service/aggregate.rs b/agent_identity/src/service/aggregate.rs index b83250a2..d10a2327 100644 --- a/agent_identity/src/service/aggregate.rs +++ b/agent_identity/src/service/aggregate.rs @@ -69,6 +69,22 @@ impl Aggregate for Service { .await .map_err(|err| MissingIdentifierError(err.to_string()))?; + #[cfg(feature = "test_utils")] + let (issuance_date, expiration_date) = { + let issuance_date = test_utils::issuance_date(); + let expiration_date = test_utils::expiration_date(); + (issuance_date, expiration_date) + }; + #[cfg(not(feature = "test_utils"))] + let (issuance_date, expiration_date) = { + let issuance_date = Timestamp::now_utc(); + let expiration_date = issuance_date + .checked_add(Duration::days(365)) + .ok_or(InvalidTimestampError)?; + + (issuance_date, expiration_date) + }; + let origin = identity_core::common::Url::parse(origin.ascii_serialization()) .map_err(|err| InvalidUrlError(err.to_string()))?; let domain_linkage_credential = DomainLinkageCredentialBuilder::new() @@ -78,13 +94,8 @@ impl Aggregate for Service { .map_err(|err| InvalidDidError(err.to_string()))?, ) .origin(origin.clone()) - .issuance_date(Timestamp::now_utc()) - // TODO: make this configurable - .expiration_date( - Timestamp::now_utc() - .checked_add(Duration::days(365)) - .ok_or(InvalidTimestampError)?, - ) + .issuance_date(issuance_date) + .expiration_date(expiration_date) .build() .map_err(|err| DomainLinkageCredentialBuilderError(err.to_string()))? .serialize_jwt(Default::default()) @@ -218,6 +229,7 @@ impl Aggregate for Service { #[cfg(test)] pub mod service_tests { + use agent_shared::config::set_config; use identity_document::service::Service as DocumentService; use super::test_utils::*; @@ -234,6 +246,8 @@ pub mod service_tests { domain_linkage_service: DocumentService, domain_linkage_resource: ServiceResource, ) { + set_config().set_preferred_did_method(agent_shared::config::SupportedDidMethod::Web); + ServiceTestFramework::with(IdentityServices::default()) .given_no_previous_events() .when(ServiceCommand::CreateDomainLinkageService { @@ -246,30 +260,33 @@ pub mod service_tests { }]) } - // #[rstest] - // #[serial_test::serial] - // async fn test_add_service( - // #[future(awt)] document: CoreDocument, - // domain_linkage_service: Service, - // #[future(awt)] document_with_domain_linkage_service: CoreDocument, - // ) { - // DocumentTestFramework::with(IdentityServices::default()) - // .given(vec![DocumentEvent::DocumentCreated { document }]) - // .when(DocumentCommand::AddService { - // service: domain_linkage_service, - // }) - // .then_expect_events(vec![DocumentEvent::ServiceAdded { - // document: document_with_domain_linkage_service, - // }]) - // } + #[rstest] + #[serial_test::serial] + async fn test_create_linked_verifiable_presentation_service( + linked_verifiable_presentation_service_id: String, + linked_verifiable_presentation_service: DocumentService, + ) { + set_config().set_preferred_did_method(agent_shared::config::SupportedDidMethod::Web); + + ServiceTestFramework::with(IdentityServices::default()) + .given_no_previous_events() + .when(ServiceCommand::CreateLinkedVerifiablePresentationService { + service_id: linked_verifiable_presentation_service_id.clone(), + presentation_ids: vec!["presentation-1".to_string()], + }) + .then_expect_events(vec![ServiceEvent::LinkedVerifiablePresentationServiceCreated { + service_id: linked_verifiable_presentation_service_id, + service: linked_verifiable_presentation_service, + }]) + } } #[cfg(feature = "test_utils")] pub mod test_utils { use super::*; - use crate::state::DOMAIN_LINKAGE_SERVICE_ID; + use crate::state::{DOMAIN_LINKAGE_SERVICE_ID, VERIFIABLE_PRESENTATION_SERVICE_ID}; use agent_shared::config::config; - use identity_core::convert::FromJson; + use identity_core::{common::Url, convert::FromJson}; use identity_document::service::{Service, ServiceEndpoint}; use rstest::*; use serde_json::json; @@ -280,9 +297,16 @@ pub mod test_utils { } #[fixture] - pub fn domain_linkage_service() -> DocumentService { + pub fn linked_verifiable_presentation_service_id() -> String { + VERIFIABLE_PRESENTATION_SERVICE_ID.to_string() + } + + #[fixture] + pub fn domain_linkage_service(did_web_identifier: String, domain_linkage_service_id: String) -> DocumentService { Service::builder(Default::default()) - .id("did:test:123#linked_domain-service".parse().unwrap()) + .id(format!("{did_web_identifier}#{domain_linkage_service_id}") + .parse() + .unwrap()) .type_("LinkedDomains") .service_endpoint( ServiceEndpoint::from_json_value(json!({ @@ -294,10 +318,48 @@ pub mod test_utils { .unwrap() } + #[fixture] + pub fn linked_verifiable_presentation_service( + did_web_identifier: String, + linked_verifiable_presentation_service_id: String, + ) -> DocumentService { + let origin = config().url.origin().ascii_serialization(); + + Service::builder(Default::default()) + .id( + format!("{did_web_identifier}#{linked_verifiable_presentation_service_id}") + .parse() + .unwrap(), + ) + .type_("LinkedVerifiablePresentation") + .service_endpoint(ServiceEndpoint::from(OrderedSet::from_iter(vec![format!( + "{origin}/v0/holder/presentations/presentation-1/signed" + ) + .parse::() + .unwrap()]))) + .build() + .unwrap() + } + + #[fixture] + pub fn did_web_identifier() -> String { + let domain = config().url.domain().unwrap().to_string(); + + format!("did:web:{domain}") + } + #[fixture] pub fn domain_linkage_resource() -> ServiceResource { - let domain_linkage_configuration = DomainLinkageConfiguration::new(vec![Jwt::from("message".to_string())]); + let domain_linkage_configuration = DomainLinkageConfiguration::new(vec![Jwt::from("eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bXktZG9tYWluLmV4YW1wbGUub3JnI2tleS0wIn0.eyJleHAiOjMxNTM2MDAwLCJpc3MiOiJkaWQ6d2ViOm15LWRvbWFpbi5leGFtcGxlLm9yZyIsIm5iZiI6MCwic3ViIjoiZGlkOndlYjpteS1kb21haW4uZXhhbXBsZS5vcmciLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vaWRlbnRpdHkuZm91bmRhdGlvbi8ud2VsbC1rbm93bi9kaWQtY29uZmlndXJhdGlvbi92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRG9tYWluTGlua2FnZUNyZWRlbnRpYWwiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsib3JpZ2luIjoiaHR0cHM6Ly9teS1kb21haW4uZXhhbXBsZS5vcmcvIn19fQ.l7dEPioa-No5zBlDCthfXDcffRB7371OnLrrQQgeAdnvHhs5F8XqRtdAWKXB8z3Se00WtGxHrTepLKmH9OWJDQ".to_string())]); ServiceResource::DomainLinkage(domain_linkage_configuration) } + + pub fn issuance_date() -> Timestamp { + Timestamp::from_unix(0).unwrap() + } + + pub fn expiration_date() -> Timestamp { + issuance_date().checked_add(Duration::days(365)).unwrap() + } } diff --git a/agent_identity/src/state.rs b/agent_identity/src/state.rs index a16f67c6..7e1b699c 100644 --- a/agent_identity/src/state.rs +++ b/agent_identity/src/state.rs @@ -51,6 +51,9 @@ impl Clone for Queries { /// The unique identifier for the linked domain service. pub const DOMAIN_LINKAGE_SERVICE_ID: &str = "linked-domain-service"; +/// The unique identifier for the linked verifiable presentation service. +pub const VERIFIABLE_PRESENTATION_SERVICE_ID: &str = "linked-verifiable-presentation-service"; + /// Initialize the identity state. pub async fn initialize(state: &IdentityState) { info!("Initializing ..."); diff --git a/agent_shared/src/config.rs b/agent_shared/src/config.rs index 1c5f4097..dcdd0c4d 100644 --- a/agent_shared/src/config.rs +++ b/agent_shared/src/config.rs @@ -280,14 +280,22 @@ impl ApplicationConfiguration { options.preferred = Some(false); } - // Set the current preferred did_method to true if available. - self.did_methods + // Set the current preferred did_method to true. + let entry = self + .did_methods .entry(preferred_did_method) .or_insert_with(|| ToggleOptions { enabled: true, preferred: Some(true), - }) - .preferred = Some(true); + }); + entry.enabled = true; + entry.preferred = Some(true); + } + + pub fn disable_did_method(&mut self, did_method: SupportedDidMethod) { + if let Some(options) = self.did_methods.get_mut(&did_method) { + options.enabled = false; + } } // TODO: make generic: set_enabled(enabled: bool) From 006858106a3d5d8e5637152aead85aab47211bde Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 4 Oct 2024 17:04:39 +0200 Subject: [PATCH 47/68] feat: add `GET` method for `/v0/services` endpoint --- agent_api_rest/src/identity/mod.rs | 9 ++- .../src/identity/services/linked_vp.rs | 69 +++++++++++++++++++ agent_api_rest/src/identity/services/mod.rs | 67 +++--------------- agent_application/docker/db/init.sql | 8 +++ .../src/service/views/all_services.rs | 23 +++++++ agent_identity/src/service/views/mod.rs | 2 + agent_identity/src/state.rs | 16 +++-- agent_store/src/in_memory.rs | 13 +++- agent_store/src/postgres.rs | 13 +++- 9 files changed, 152 insertions(+), 68 deletions(-) create mode 100644 agent_api_rest/src/identity/services/linked_vp.rs create mode 100644 agent_identity/src/service/views/all_services.rs diff --git a/agent_api_rest/src/identity/mod.rs b/agent_api_rest/src/identity/mod.rs index 40cb1c09..90bbc1f0 100644 --- a/agent_api_rest/src/identity/mod.rs +++ b/agent_api_rest/src/identity/mod.rs @@ -6,14 +6,19 @@ use axum::{ routing::{get, post}, Router, }; -use services::linked_vp; +use services::{linked_vp::linked_vp, services}; use well_known::{did::did, did_configuration::did_configuration}; use crate::API_VERSION; pub fn router(identity_state: IdentityState) -> Router { Router::new() - .nest(API_VERSION, Router::new().route("/services/linked-vp", post(linked_vp))) + .nest( + API_VERSION, + Router::new() + .route("/services", get(services)) + .route("/services/linked-vp", post(linked_vp)), + ) .route("/.well-known/did.json", get(did)) .route("/.well-known/did-configuration.json", get(did_configuration)) .with_state(identity_state) diff --git a/agent_api_rest/src/identity/services/linked_vp.rs b/agent_api_rest/src/identity/services/linked_vp.rs new file mode 100644 index 00000000..cee2f425 --- /dev/null +++ b/agent_api_rest/src/identity/services/linked_vp.rs @@ -0,0 +1,69 @@ +use agent_identity::{ + document::command::DocumentCommand, + service::{aggregate::Service, command::ServiceCommand}, + state::IdentityState, +}; +use agent_shared::handlers::{command_handler, query_handler}; +use axum::{ + extract::State, + response::{IntoResponse, Response}, + Json, +}; +use did_manager::DidMethod; +use hyper::StatusCode; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tracing::info; + +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LinkedVPEndpointRequest { + pub presentation_ids: Vec, +} + +#[axum_macros::debug_handler] +pub(crate) async fn linked_vp(State(state): State, Json(payload): Json) -> Response { + info!("Request Body: {}", payload); + + let Ok(LinkedVPEndpointRequest { presentation_ids }) = serde_json::from_value(payload) else { + return (StatusCode::BAD_REQUEST, "invalid payload").into_response(); + }; + + let service_id = "linked-verifiable-presentation-service".to_string(); + let command = ServiceCommand::CreateLinkedVerifiablePresentationService { + service_id: service_id.clone(), + presentation_ids, + }; + + // Create a linked verifiable presentation service. + if command_handler(&service_id, &state.command.service, command) + .await + .is_err() + { + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + + let linked_verifiable_presentation_service = match query_handler(&service_id, &state.query.service).await { + Ok(Some(Service { + service: Some(linked_verifiable_presentation_service), + .. + })) => linked_verifiable_presentation_service, + _ => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), + }; + + let command = DocumentCommand::AddService { + service: linked_verifiable_presentation_service, + }; + + if command_handler(&DidMethod::Web.to_string(), &state.command.document, command) + .await + .is_err() + { + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + + match query_handler(&DidMethod::Web.to_string(), &state.query.document).await { + Ok(Some(document)) => (StatusCode::OK, Json(document)).into_response(), + _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } +} diff --git a/agent_api_rest/src/identity/services/mod.rs b/agent_api_rest/src/identity/services/mod.rs index cee2f425..d18000ff 100644 --- a/agent_api_rest/src/identity/services/mod.rs +++ b/agent_api_rest/src/identity/services/mod.rs @@ -1,69 +1,20 @@ -use agent_identity::{ - document::command::DocumentCommand, - service::{aggregate::Service, command::ServiceCommand}, - state::IdentityState, -}; -use agent_shared::handlers::{command_handler, query_handler}; +pub mod linked_vp; + +use agent_identity::state::IdentityState; +use agent_shared::handlers::query_handler; use axum::{ extract::State, response::{IntoResponse, Response}, Json, }; -use did_manager::DidMethod; use hyper::StatusCode; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use tracing::info; - -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct LinkedVPEndpointRequest { - pub presentation_ids: Vec, -} +use serde_json::json; #[axum_macros::debug_handler] -pub(crate) async fn linked_vp(State(state): State, Json(payload): Json) -> Response { - info!("Request Body: {}", payload); - - let Ok(LinkedVPEndpointRequest { presentation_ids }) = serde_json::from_value(payload) else { - return (StatusCode::BAD_REQUEST, "invalid payload").into_response(); - }; - - let service_id = "linked-verifiable-presentation-service".to_string(); - let command = ServiceCommand::CreateLinkedVerifiablePresentationService { - service_id: service_id.clone(), - presentation_ids, - }; - - // Create a linked verifiable presentation service. - if command_handler(&service_id, &state.command.service, command) - .await - .is_err() - { - return StatusCode::INTERNAL_SERVER_ERROR.into_response(); - } - - let linked_verifiable_presentation_service = match query_handler(&service_id, &state.query.service).await { - Ok(Some(Service { - service: Some(linked_verifiable_presentation_service), - .. - })) => linked_verifiable_presentation_service, - _ => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), - }; - - let command = DocumentCommand::AddService { - service: linked_verifiable_presentation_service, - }; - - if command_handler(&DidMethod::Web.to_string(), &state.command.document, command) - .await - .is_err() - { - return StatusCode::INTERNAL_SERVER_ERROR.into_response(); - } - - match query_handler(&DidMethod::Web.to_string(), &state.query.document).await { - Ok(Some(document)) => (StatusCode::OK, Json(document)).into_response(), +pub(crate) async fn services(State(state): State) -> Response { + match query_handler("all_services", &state.query.all_services).await { + Ok(Some(all_services_view)) => (StatusCode::OK, Json(all_services_view)).into_response(), + Ok(None) => (StatusCode::OK, Json(json!({}))).into_response(), _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } } diff --git a/agent_application/docker/db/init.sql b/agent_application/docker/db/init.sql index 88bf0d1f..99e11ff4 100644 --- a/agent_application/docker/db/init.sql +++ b/agent_application/docker/db/init.sql @@ -26,6 +26,14 @@ CREATE TABLE service PRIMARY KEY (view_id) ); +CREATE TABLE all_services +( + view_id text NOT NULL, + version bigint CHECK (version >= 0) NOT NULL, + payload json NOT NULL, + PRIMARY KEY (view_id) +); + CREATE TABLE offer ( view_id text NOT NULL, diff --git a/agent_identity/src/service/views/all_services.rs b/agent_identity/src/service/views/all_services.rs new file mode 100644 index 00000000..58551d2f --- /dev/null +++ b/agent_identity/src/service/views/all_services.rs @@ -0,0 +1,23 @@ +use super::ServiceView; +use crate::service::aggregate::Service; +use cqrs_es::{EventEnvelope, View}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct AllServicesView { + #[serde(flatten)] + pub services: HashMap, +} + +impl View for AllServicesView { + fn update(&mut self, event: &EventEnvelope) { + self.services + // Get the entry for the aggregate_id + .entry(event.aggregate_id.clone()) + // or insert a new one if it doesn't exist + .or_default() + // update the view with the event + .update(event); + } +} diff --git a/agent_identity/src/service/views/mod.rs b/agent_identity/src/service/views/mod.rs index e7e4bad1..ca6e6df0 100644 --- a/agent_identity/src/service/views/mod.rs +++ b/agent_identity/src/service/views/mod.rs @@ -1,3 +1,5 @@ +pub mod all_services; + use super::aggregate::Service; use cqrs_es::{EventEnvelope, View}; diff --git a/agent_identity/src/state.rs b/agent_identity/src/state.rs index 7e1b699c..c6400198 100644 --- a/agent_identity/src/state.rs +++ b/agent_identity/src/state.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use tracing::{info, warn}; use crate::document::command::DocumentCommand; +use crate::service::views::all_services::AllServicesView; use crate::{ document::{aggregate::Document, views::DocumentView}, service::{aggregate::Service, command::ServiceCommand, views::ServiceView}, @@ -28,15 +29,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 D: ViewRepository + ?Sized, - S: ViewRepository + ?Sized, + S1: ViewRepository + ?Sized, + S2: ViewRepository + ?Sized, { pub document: Arc, - pub service: Arc, + pub service: Arc, + pub all_services: Arc, } impl Clone for Queries { @@ -44,6 +51,7 @@ impl Clone for Queries { ViewRepositories { document: self.document.clone(), service: self.service.clone(), + all_services: self.all_services.clone(), } } } diff --git a/agent_store/src/in_memory.rs b/agent_store/src/in_memory.rs index 806128c2..9880538c 100644 --- a/agent_store/src/in_memory.rs +++ b/agent_store/src/in_memory.rs @@ -123,6 +123,10 @@ pub async fn identity_state( // Initialize the in-memory repositories. let document = Arc::new(MemRepository::default()); let service = Arc::new(MemRepository::default()); + let all_services = Arc::new(MemRepository::default()); + + // Create custom-queries for the offer aggregate. + let all_services_query = ListAllQuery::new(all_services.clone(), "all_services"); // Partition the event_publishers into the different aggregates. let Partitions { @@ -145,12 +149,17 @@ pub async fn identity_state( service_event_publishers.into_iter().fold( AggregateHandler::new(identity_services) .append_query(SimpleLoggingQuery {}) - .append_query(generic_query(service.clone())), + .append_query(generic_query(service.clone())) + .append_query(all_services_query), |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), ), ), }, - query: agent_identity::state::ViewRepositories { document, service }, + query: agent_identity::state::ViewRepositories { + document, + service, + all_services, + }, } } diff --git a/agent_store/src/postgres.rs b/agent_store/src/postgres.rs index b421d50d..ba97d37a 100644 --- a/agent_store/src/postgres.rs +++ b/agent_store/src/postgres.rs @@ -78,6 +78,10 @@ pub async fn identity_state( // Initialize the postgres repositories. let document = Arc::new(PostgresViewRepository::new("document", pool.clone())); let service = Arc::new(PostgresViewRepository::new("service", pool.clone())); + let all_services = Arc::new(PostgresViewRepository::new("all_services", pool.clone())); + + // Create custom-queries for the offer aggregate. + let all_services_query = ListAllQuery::new(all_services.clone(), "all_services"); // Partition the event_publishers into the different aggregates. let Partitions { @@ -100,12 +104,17 @@ pub async fn identity_state( service_event_publishers.into_iter().fold( AggregateHandler::new(pool.clone(), identity_services) .append_query(SimpleLoggingQuery {}) - .append_query(generic_query(service.clone())), + .append_query(generic_query(service.clone())) + .append_query(all_services_query), |aggregate_handler, event_publisher| aggregate_handler.append_event_publisher(event_publisher), ), ), }, - query: agent_identity::state::ViewRepositories { document, service }, + query: agent_identity::state::ViewRepositories { + document, + service, + all_services, + }, } } From 610af21a4aecbe4502d025b9fdf24de7c3017aeb Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 4 Oct 2024 17:05:47 +0200 Subject: [PATCH 48/68] test: update Postman collection --- .../postman/ssi-agent.postman_collection.json | 121 ++++++++++++------ 1 file changed, 83 insertions(+), 38 deletions(-) diff --git a/agent_api_rest/postman/ssi-agent.postman_collection.json b/agent_api_rest/postman/ssi-agent.postman_collection.json index c4c73280..2cb20fdf 100644 --- a/agent_api_rest/postman/ssi-agent.postman_collection.json +++ b/agent_api_rest/postman/ssi-agent.postman_collection.json @@ -26,16 +26,6 @@ "type": "text/javascript", "packages": {} } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } } ], "request": { @@ -43,7 +33,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"offerId\": {{OFFER_ID}},\n \"credentialConfigurationId\": \"openbadge_credential\",\n \"credential\":{\n \"credentialSubject\":{\n \"type\":[\"AchievementSubject\"],\n \"achievement\": {\n \"id\": \"https://demo.edubadges.nl/public/assertions/DAO4oUapQ_eJr9VwMz6jIQ\",\n \"type\": \"Achievement\",\n \"criteria\":{\"narrative\": \"testtesttesttesttesttesttest\"},\n \"description\": \"testtesttesttesttesttesttesttest\",\n \"name\": \"Geschiedenis van de Oudheid\",\n \"image\":{\n \"id\": \"https://api-demo.edubadges.nl/media/uploads/badges/issuer_badgeclass_3e51ca72-ee9b-493b-b787-fd89f2df3189.png\",\n \"type\": \"Image\"\n }\n }\n }\n }\n }", + "raw": "{\n \"offerId\":\"{{OFFER_ID}}\",\n \"credentialConfigurationId\": \"openbadge_credential\",\n \"credential\": {\n \"credentialSubject\": {\n \"type\": [ \"AchievementSubject\" ],\n \"achievement\": {\n \"id\": \"https://example.com/achievements/21st-century-skills/teamwork\",\n \"type\": \"Achievement\",\n \"criteria\": {\n \"narrative\": \"Team members are nominated for this badge by their peers and recognized upon review by Example Corp management.\"\n },\n \"description\": \"This badge recognizes the development of the capacity to collaborate within a group environment.\",\n \"name\": \"Teamwork\"\n }\n }\n }\n}", "options": { "raw": { "language": "json" @@ -84,8 +74,11 @@ { "listen": "prerequest", "script": { - "exec": [], - "type": "text/javascript" + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} } } ], @@ -115,7 +108,7 @@ "response": [] }, { - "name": "temp_thuiswinkel_credential", + "name": "Thuiswinkel Waarborg Credential", "event": [ { "listen": "test", @@ -148,7 +141,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"offerId\":\"{{OFFER_ID}}\",\n \"credentialConfigurationId\": \"w3c_vc_credential\",\n \"credential\": {\n \"credentialSubject\": {\n \"id\": \"https://ecommerce.impierce.com/\",\n \"image\": \"https://www.thuiswinkel.org/Images/logo-thuiswinkel_waarborg.svg\",\n \"name\": \"Impierce Bar\",\n \"certificaat\": {\n \"type\": \"ThuiswinkelWaarborg\",\n \"certificeringsDatum\": \"2024-06-26\",\n \"geldigheidsPeriode\": \"1 jaar\",\n \"garanties\": [\n \"Het bedrijf is echt en bereikbaar.\",\n \"Voldoet aan de Thuiswinkel Algemene Voorwaarden.\",\n \"14 dagen bedenktijd.\",\n \"Veilige betaalmethoden.\",\n \"Duidelijke product/servicebeschrijvingen.\",\n \"Transparant bestelproces.\",\n \"Duidelijke prijzen.\",\n \"Veilige betaalomgeving.\",\n \"Veilige omgang met persoonlijke gegevens.\",\n \"Effectieve klachtenafhandeling en onafhankelijke geschillenbemiddeling.\"\n ]\n }\n }\n }\n}", + "raw": "{\n \"offerId\":\"{{OFFER_ID}}\",\n \"credentialConfigurationId\": \"w3c_vc_credential\",\n \"credential\": {\n \"credentialSubject\": {\n \"id\": \"https://ecommerce.impierce.com/\",\n \"image\": \"https://www.thuiswinkel.org/Images/logo-thuiswinkel_waarborg.svg\",\n \"name\": \"VirtualVendors\",\n \"certificaat\": {\n \"type\": \"ThuiswinkelWaarborg\",\n \"certificeringsDatum\": \"2024-06-26\",\n \"geldigheidsPeriode\": \"1 jaar\",\n \"garanties\": [\n \"Het bedrijf is echt en bereikbaar.\",\n \"Voldoet aan de Thuiswinkel Algemene Voorwaarden.\",\n \"14 dagen bedenktijd.\",\n \"Veilige betaalmethoden.\",\n \"Duidelijke product/servicebeschrijvingen.\",\n \"Transparant bestelproces.\",\n \"Duidelijke prijzen.\",\n \"Veilige betaalomgeving.\",\n \"Veilige omgang met persoonlijke gegevens.\",\n \"Effectieve klachtenafhandeling en onafhankelijke geschillenbemiddeling.\"\n ]\n }\n }\n }\n}", "options": { "raw": { "language": "json" @@ -793,8 +786,11 @@ { "listen": "prerequest", "script": { - "exec": [], - "type": "text/javascript" + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} } } ], @@ -815,25 +811,6 @@ }, "response": [] }, - { - "name": "credentials", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{HOST}}/v0/holder/credentials", - "host": [ - "{{HOST}}" - ], - "path": [ - "v0", - "holder", - "credentials" - ] - } - }, - "response": [] - }, { "name": "offers_accept", "request": { @@ -878,6 +855,26 @@ }, { "name": "credentials", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const jsonData = JSON.parse(responseBody);", + "", + "if (jsonData && typeof jsonData === 'object') {", + " const holderCredentialId = Object.keys(jsonData)[0];", + "", + " if (holderCredentialId) {", + " pm.collectionVariables.set(\"HOLDER_CREDENTIAL_ID\", holderCredentialId);", + " }", + "}" + ], + "type": "text/javascript", + "packages": {} + } + } + ], "request": { "method": "GET", "header": [], @@ -897,6 +894,26 @@ }, { "name": "presentations", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const jsonData = JSON.parse(responseBody);", + "", + "if (jsonData && typeof jsonData === 'object') {", + " const presentationId = Object.keys(jsonData)[0];", + "", + " if (presentationId) {", + " pm.collectionVariables.set(\"PRESENTATION_ID\", presentationId);", + " }", + "}" + ], + "type": "text/javascript", + "packages": {} + } + } + ], "request": { "method": "GET", "header": [], @@ -921,7 +938,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"credentialIds\": [\"credential_id\"]\n}", + "raw": "{\n \"credentialIds\": [\"{{HOLDER_CREDENTIAL_ID}}\"]\n}", "options": { "raw": { "language": "json" @@ -954,7 +971,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"presentationId\": \"presentation_id\"\n}", + "raw": "{\n \"presentationIds\": [\"{{PRESENTATION_ID}}\"]\n}", "options": { "raw": { "language": "json" @@ -974,6 +991,24 @@ } }, "response": [] + }, + { + "name": "services", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/v0/services", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "services" + ] + } + }, + "response": [] } ] } @@ -1043,6 +1078,16 @@ "key": "RECEIVED_OFFER_ID", "value": "INITIAL_VALUE", "type": "string" + }, + { + "key": "HOLDER_CREDENTIAL_ID", + "value": "INITIAL_VALUE", + "type": "string" + }, + { + "key": "PRESENTATION_ID", + "value": "INITIAL_VALUE", + "type": "string" } ] } \ No newline at end of file From 2ff7d34bf153c88982b55e562c7d60c2244e871c Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 4 Oct 2024 19:58:45 +0200 Subject: [PATCH 49/68] docs: add document, service and presentation events --- agent_event_publisher_http/README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/agent_event_publisher_http/README.md b/agent_event_publisher_http/README.md index 2fa08dbf..cdc0a663 100644 --- a/agent_event_publisher_http/README.md +++ b/agent_event_publisher_http/README.md @@ -21,6 +21,20 @@ event_publishers: ### Available events +#### `document` + +``` +DocumentCreated +ServiceAdded +``` + +#### `service` + +``` +DomainLinkageServiceCreated +LinkedVerifiablePresentationServiceCreated +``` + #### `credential` ``` @@ -53,6 +67,12 @@ CredentialConfigurationAdded CredentialAdded ``` +#### `presentation` + +``` +PresentationCreated +``` + #### `received_offer` ``` From 3b4987f2d2a66b5e3f48c88a2eb0b79033b1c3b0 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 4 Oct 2024 21:25:48 +0200 Subject: [PATCH 50/68] fix: remove unused import --- agent_holder/src/presentation/aggregate.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/agent_holder/src/presentation/aggregate.rs b/agent_holder/src/presentation/aggregate.rs index 456257a2..4d3492a4 100644 --- a/agent_holder/src/presentation/aggregate.rs +++ b/agent_holder/src/presentation/aggregate.rs @@ -4,7 +4,7 @@ use agent_shared::config::{get_preferred_did_method, get_preferred_signing_algor use async_trait::async_trait; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use cqrs_es::Aggregate; -use identity_core::{common::Timestamp, convert::ToJson}; +use identity_core::convert::ToJson; use identity_credential::{credential::Jwt, presentation::JwtPresentationOptions}; use jsonwebtoken::Header; use serde::{Deserialize, Serialize}; @@ -60,7 +60,8 @@ impl Aggregate for Presentation { } #[cfg(feature = "test_utils")] - let options = JwtPresentationOptions::default().issuance_date(Timestamp::from_unix(0).unwrap()); + let options = JwtPresentationOptions::default() + .issuance_date(identity_core::common::Timestamp::from_unix(0).unwrap()); #[cfg(not(feature = "test_utils"))] let options = JwtPresentationOptions::default(); From a8b5dc41e360a5b3a2d81559f8b5ce2f5b61f2bc Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Sat, 5 Oct 2024 13:40:29 +0200 Subject: [PATCH 51/68] ci: add DS_Store to .gitignore file --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index d43a0f1e..a2fcc431 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ **/*.env !**/.env.example + +.DS_Store From ca3a9ed0171bdee8d6fc2fe66e4517ce2388c112 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Sat, 5 Oct 2024 23:34:56 +0200 Subject: [PATCH 52/68] feat: add Document and Service to config.rs --- agent_shared/src/config.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/agent_shared/src/config.rs b/agent_shared/src/config.rs index dcdd0c4d..4eb1d2e3 100644 --- a/agent_shared/src/config.rs +++ b/agent_shared/src/config.rs @@ -116,6 +116,10 @@ pub struct EventPublisherHttp { #[derive(Debug, Deserialize, Clone, Default)] pub struct Events { + #[serde(default)] + pub document: Vec, + #[serde(default)] + pub service: Vec, #[serde(default)] pub server_config: Vec, #[serde(default)] @@ -132,6 +136,18 @@ pub struct Events { pub authorization_request: Vec, } +#[derive(Debug, Serialize, Deserialize, Clone, strum::Display)] +pub enum DocumentEvent { + DocumentCreated, + ServiceAdded, +} + +#[derive(Debug, Serialize, Deserialize, Clone, strum::Display)] +pub enum ServiceEvent { + DomainLinkageServiceCreated, + LinkedVerifiablePresentationServiceCreated, +} + #[derive(Debug, Serialize, Deserialize, Clone, strum::Display)] pub enum ServerConfigEvent { ServerMetadataInitialized, From ae832565b56391e45b35234091d5b026fb50f012 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Mon, 7 Oct 2024 14:33:35 +0200 Subject: [PATCH 53/68] fix: update .env.example variables --- .env.example | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index b0a488c7..7cfdf006 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,12 @@ UNICORE__LOG_FORMAT=text -UNICORE__EVENT_STORE__TYPE=postgres -UNICORE__EVENT_STORE__CONNECTION_STRING="postgresql://demo_user:demo_pass@cqrs-postgres-db:5432/demo" +UNICORE__EVENT_STORE__TYPE=in_memory +# UNICORE__EVENT_STORE__CONNECTION_STRING="postgresql://demo_user:demo_pass@cqrs-postgres-db:5432/demo" -UNICORE__SECRET_MANAGER__STRONGHOLD_PATH="agent_secret_manager/tests/res/test.stronghold" +UNICORE__URL="http://localhost:3033" + +UNICORE__SECRET_MANAGER__STRONGHOLD_PATH="agent_secret_manager/tests/res/temp.stronghold" +# UNICORE__SECRET_MANAGER__STRONGHOLD_PATH="agent_secret_manager/tests/res/test.stronghold" UNICORE__SECRET_MANAGER__STRONGHOLD_PASSWORD="secure_password" -UNICORE__SECRET_MANAGER__ISSUER_EDDSA_KEY_ID="9O66nzWqYYy1LmmiOudOlh2SMIaUWoTS" -UNICORE__SECRET_MANAGER__ISSUER_DID="did:iota:rms:0x42ad588322e58b3c07aa39e4948d021ee17ecb5747915e9e1f35f028d7ecaf90" -UNICORE__SECRET_MANAGER__ISSUER_FRAGMENT="bQKQRzaop7CgEvqVq8UlgLGsdF-R-hnLFkKFZqW2VN0" +# UNICORE__SECRET_MANAGER__ISSUER_KEY_ID="9O66nzWqYYy1LmmiOudOlh2SMIaUWoTS" +# UNICORE__SECRET_MANAGER__ISSUER_DID="did:iota:rms:0x42ad588322e58b3c07aa39e4948d021ee17ecb5747915e9e1f35f028d7ecaf90" +# UNICORE__SECRET_MANAGER__ISSUER_FRAGMENT="bQKQRzaop7CgEvqVq8UlgLGsdF-R-hnLFkKFZqW2VN0" From 6f614e851909846f90b0fa04848b3ab4767e62d2 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Mon, 7 Oct 2024 20:40:31 +0200 Subject: [PATCH 54/68] feat: make `/accept` endpoint respond with the Offer --- Cargo.lock | 1 + .../src/holder/holder/offers/accept.rs | 18 ++++--- agent_holder/Cargo.toml | 3 +- agent_holder/src/credential/aggregate.rs | 47 +++++++++++++++++-- agent_holder/src/credential/error.rs | 5 +- agent_holder/src/credential/event.rs | 3 ++ agent_holder/src/credential/queries/mod.rs | 2 + agent_holder/src/offer/aggregate.rs | 38 +++++++++++---- agent_holder/src/offer/event.rs | 5 +- agent_holder/src/presentation/aggregate.rs | 9 +++- 10 files changed, 104 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6afb8a94..79046a59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -179,6 +179,7 @@ dependencies = [ "tower", "tracing", "tracing-test", + "uuid", ] [[package]] diff --git a/agent_api_rest/src/holder/holder/offers/accept.rs b/agent_api_rest/src/holder/holder/offers/accept.rs index 9022f14b..ead0e36a 100644 --- a/agent_api_rest/src/holder/holder/offers/accept.rs +++ b/agent_api_rest/src/holder/holder/offers/accept.rs @@ -1,12 +1,13 @@ use agent_holder::{ credential::command::CredentialCommand, - offer::{command::OfferCommand, queries::ReceivedOfferView}, + offer::{aggregate::OfferCredential, command::OfferCommand, queries::ReceivedOfferView}, state::HolderState, }; use agent_shared::handlers::{command_handler, query_handler}; use axum::{ extract::{Path, State}, response::{IntoResponse, Response}, + Json, }; use hyper::StatusCode; @@ -50,9 +51,11 @@ pub(crate) async fn accept(State(state): State, Path(offer_id): Pat _ => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), }; - for credential in credentials { - let credential_id = uuid::Uuid::new_v4().to_string(); - + for OfferCredential { + credential_id, + credential, + } in credentials + { let command = CredentialCommand::AddCredential { credential_id: credential_id.clone(), offer_id: offer_id.clone(), @@ -68,6 +71,9 @@ pub(crate) async fn accept(State(state): State, Path(offer_id): Pat } } - // TODO: What do we return here? - StatusCode::OK.into_response() + match query_handler(&offer_id, &state.query.received_offer).await { + Ok(Some(received_offer_view)) => (StatusCode::OK, Json(received_offer_view)).into_response(), + Ok(None) => StatusCode::NOT_FOUND.into_response(), + _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } } diff --git a/agent_holder/Cargo.toml b/agent_holder/Cargo.toml index b7b9e004..0ac7a60e 100644 --- a/agent_holder/Cargo.toml +++ b/agent_holder/Cargo.toml @@ -20,6 +20,7 @@ serde.workspace = true serde_json.workspace = true thiserror.workspace = true tracing.workspace = true +uuid.workspace = true # `test_utils` dependencies rstest = { workspace = true, optional = true } @@ -32,6 +33,7 @@ agent_secret_manager = { path = "../agent_secret_manager", features = ["test_uti agent_shared = { path = "../agent_shared", features = ["test_utils"] } agent_store = { path = "../agent_store" } +async-std = { version = "1.5", features = ["attributes", "tokio1"] } axum.workspace = true did_manager.workspace = true lazy_static.workspace = true @@ -43,7 +45,6 @@ serial_test = "3.0" tokio.workspace = true tower.workspace = true tracing-test.workspace = true -async-std = { version = "1.5", features = ["attributes", "tokio1"] } [features] test_utils = ["dep:rstest"] diff --git a/agent_holder/src/credential/aggregate.rs b/agent_holder/src/credential/aggregate.rs index d99f2438..9981d3db 100644 --- a/agent_holder/src/credential/aggregate.rs +++ b/agent_holder/src/credential/aggregate.rs @@ -3,17 +3,24 @@ use crate::credential::error::CredentialError::{self}; use crate::credential::event::CredentialEvent; use crate::services::HolderServices; use async_trait::async_trait; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use cqrs_es::Aggregate; use identity_credential::credential::Jwt; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tracing::info; +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct Data { + pub raw: serde_json::Value, +} + #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Credential { pub credential_id: Option, pub offer_id: Option, pub signed: Option, + pub data: Option, } #[async_trait] @@ -33,6 +40,7 @@ impl Aggregate for Credential { _services: &Self::Services, ) -> Result, Self::Error> { use CredentialCommand::*; + use CredentialError::*; use CredentialEvent::*; info!("Handling command: {:?}", command); @@ -42,11 +50,19 @@ impl Aggregate for Credential { credential_id, offer_id, credential, - } => Ok(vec![CredentialAdded { - credential_id, - offer_id, - credential, - }]), + } => { + let raw = get_unverified_jwt_claims(&serde_json::json!(credential))? + .get("vc") + .cloned() + .ok_or(CredentialDecodingError)?; + + Ok(vec![CredentialAdded { + credential_id, + offer_id, + credential, + data: Data { raw }, + }]) + } } } @@ -60,15 +76,31 @@ impl Aggregate for Credential { credential_id, offer_id, credential, + data, } => { self.credential_id = Some(credential_id); self.offer_id = Some(offer_id); self.signed = Some(credential); + self.data = Some(data); } } } } +// TODO: actually validate the JWT! +/// Get the claims from a JWT without performing validation. +pub fn get_unverified_jwt_claims(jwt: &serde_json::Value) -> Result { + jwt.as_str() + .and_then(|string| string.splitn(3, '.').collect::>().get(1).cloned()) + .and_then(|payload| { + URL_SAFE_NO_PAD + .decode(payload) + .ok() + .and_then(|payload_bytes| serde_json::from_slice::(&payload_bytes).ok()) + }) + .ok_or(CredentialError::CredentialDecodingError) +} + #[cfg(test)] pub mod credential_tests { use super::test_utils::*; @@ -97,6 +129,11 @@ pub mod credential_tests { credential_id, offer_id, credential: Jwt::from(OPENBADGE_VERIFIABLE_CREDENTIAL_JWT.to_string()), + data: Data { + raw: get_unverified_jwt_claims(&serde_json::json!(OPENBADGE_VERIFIABLE_CREDENTIAL_JWT)).unwrap() + ["vc"] + .clone(), + }, }]) } } diff --git a/agent_holder/src/credential/error.rs b/agent_holder/src/credential/error.rs index df235841..314ec188 100644 --- a/agent_holder/src/credential/error.rs +++ b/agent_holder/src/credential/error.rs @@ -1,4 +1,7 @@ use thiserror::Error; #[derive(Error, Debug)] -pub enum CredentialError {} +pub enum CredentialError { + #[error("Failed to decode Credential JWT")] + CredentialDecodingError, +} diff --git a/agent_holder/src/credential/event.rs b/agent_holder/src/credential/event.rs index 40e7c9eb..9a41dcb7 100644 --- a/agent_holder/src/credential/event.rs +++ b/agent_holder/src/credential/event.rs @@ -2,12 +2,15 @@ use cqrs_es::DomainEvent; use identity_credential::credential::Jwt; use serde::{Deserialize, Serialize}; +use super::aggregate::Data; + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum CredentialEvent { CredentialAdded { credential_id: String, offer_id: String, credential: Jwt, + data: Data, }, } diff --git a/agent_holder/src/credential/queries/mod.rs b/agent_holder/src/credential/queries/mod.rs index 25cca81b..fa5d5560 100644 --- a/agent_holder/src/credential/queries/mod.rs +++ b/agent_holder/src/credential/queries/mod.rs @@ -15,10 +15,12 @@ impl View for Credential { credential_id, offer_id, credential, + data, } => { self.credential_id.replace(credential_id.clone()); self.offer_id.replace(offer_id.clone()); self.signed.replace(credential.clone()); + self.data.replace(data.clone()); } } } diff --git a/agent_holder/src/offer/aggregate.rs b/agent_holder/src/offer/aggregate.rs index efbfce69..30d84c04 100644 --- a/agent_holder/src/offer/aggregate.rs +++ b/agent_holder/src/offer/aggregate.rs @@ -20,10 +20,16 @@ pub enum Status { #[default] Pending, Accepted, - Received, + CredentialsReceived, Rejected, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct OfferCredential { + pub credential_id: String, + pub credential: Jwt, +} + #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Offer { pub credential_offer: Option, @@ -33,7 +39,7 @@ pub struct Offer { // 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 credentials: Vec, } #[async_trait] @@ -179,7 +185,7 @@ impl Aggregate for Offer { .as_ref() .ok_or(MissingCredentialConfigurationsError)?; - let credentials: Vec = match credential_configuration_ids.len() { + let credentials: Vec = match credential_configuration_ids.len() { 0 => vec![], 1 => { let credential_configuration_id = &credential_configuration_ids[0]; @@ -203,7 +209,15 @@ impl Aggregate for Offer { } }; - vec![credential] + #[cfg(not(feature = "test_utils"))] + let credential_id = uuid::Uuid::new_v4().to_string(); + #[cfg(feature = "test_utils")] + let credential_id = test_utils::credential_id(); + + vec![OfferCredential { + credential_id, + credential, + }] } _batch => { return Err(BatchCredentialRequestError); @@ -214,7 +228,7 @@ impl Aggregate for Offer { Ok(vec![CredentialResponseReceived { offer_id, - status: Status::Received, + status: Status::CredentialsReceived, credentials, }]) } @@ -426,7 +440,7 @@ pub mod tests { #[future(awt)] credential_offer_parameters: Box, #[future(awt)] token_response: TokenResponse, credential_configurations_supported: HashMap, - signed_credentials: Vec, + signed_credentials: Vec, ) { OfferTestFramework::with(Service::default()) .given(vec![ @@ -450,7 +464,7 @@ pub mod tests { .await .then_expect_events(vec![OfferEvent::CredentialResponseReceived { offer_id: offer_id.clone(), - status: Status::Received, + status: Status::CredentialsReceived, credentials: signed_credentials, }]); } @@ -482,6 +496,7 @@ pub mod tests { #[cfg(feature = "test_utils")] pub mod test_utils { + use super::*; use agent_shared::generate_random_string; use identity_credential::credential::Jwt; use rstest::*; @@ -492,7 +507,12 @@ pub mod test_utils { } #[fixture] - pub fn signed_credentials() -> Vec { - vec![Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJkZWdyZWUiOnsidHlwZSI6Ik1hc3RlckRlZ3JlZSIsIm5hbWUiOiJNYXN0ZXIgb2YgT2NlYW5vZ3JhcGh5In0sImZpcnN0X25hbWUiOiJGZXJyaXMiLCJsYXN0X25hbWUiOiJSdXN0YWNlYW4ifSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiJ9fQ.jQEpI7DhjOcmyhPEpfGARwcRyzor_fUvynb43-eqD9175FBoshENX0S-8qlloQ7vbT5gat8TjvcDlGDN720ZBw".to_string())] + pub fn credential_id() -> String { + "credential_id".to_string() + } + + #[fixture] + pub fn signed_credentials(credential_id: String) -> Vec { + vec![OfferCredential { credential_id, credential: Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJkZWdyZWUiOnsidHlwZSI6Ik1hc3RlckRlZ3JlZSIsIm5hbWUiOiJNYXN0ZXIgb2YgT2NlYW5vZ3JhcGh5In0sImZpcnN0X25hbWUiOiJGZXJyaXMiLCJsYXN0X25hbWUiOiJSdXN0YWNlYW4ifSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiJ9fQ.jQEpI7DhjOcmyhPEpfGARwcRyzor_fUvynb43-eqD9175FBoshENX0S-8qlloQ7vbT5gat8TjvcDlGDN720ZBw".to_string())}] } } diff --git a/agent_holder/src/offer/event.rs b/agent_holder/src/offer/event.rs index 4f3753fc..a3f9da8f 100644 --- a/agent_holder/src/offer/event.rs +++ b/agent_holder/src/offer/event.rs @@ -1,6 +1,5 @@ -use super::aggregate::Status; +use super::aggregate::{OfferCredential, Status}; use cqrs_es::DomainEvent; -use identity_credential::credential::Jwt; use oid4vci::{ credential_issuer::credential_configurations_supported::CredentialConfigurationsSupportedObject, credential_offer::CredentialOfferParameters, token_response::TokenResponse, @@ -26,7 +25,7 @@ pub enum OfferEvent { CredentialResponseReceived { offer_id: String, status: Status, - credentials: Vec, + credentials: Vec, }, CredentialOfferRejected { offer_id: String, diff --git a/agent_holder/src/presentation/aggregate.rs b/agent_holder/src/presentation/aggregate.rs index 4d3492a4..ffb07230 100644 --- a/agent_holder/src/presentation/aggregate.rs +++ b/agent_holder/src/presentation/aggregate.rs @@ -132,6 +132,7 @@ impl Aggregate for Presentation { pub mod presentation_tests { use crate::offer::aggregate::test_utils::signed_credentials; + use crate::offer::aggregate::OfferCredential; use super::test_utils::*; use super::*; @@ -143,12 +144,16 @@ pub mod presentation_tests { #[rstest] #[serial_test::serial] - async fn test_create_presentation(presentation_id: String, signed_credentials: Vec, signed_presentation: Jwt) { + async fn test_create_presentation( + presentation_id: String, + signed_credentials: Vec, + signed_presentation: Jwt, + ) { PresentationTestFramework::with(Service::default()) .given_no_previous_events() .when(PresentationCommand::CreatePresentation { presentation_id: presentation_id.clone(), - signed_credentials, + signed_credentials: signed_credentials.into_iter().map(|c| c.credential).collect(), }) .then_expect_events(vec![PresentationEvent::PresentationCreated { presentation_id, From 3526cb12dcb3df7896f199ae28b6040de34cb391 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Tue, 15 Oct 2024 18:32:40 +0200 Subject: [PATCH 55/68] feat: change `openid4vci/offers` from GET to POST --- agent_api_rest/src/holder/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent_api_rest/src/holder/mod.rs b/agent_api_rest/src/holder/mod.rs index 0cb17c93..6ffc2d7d 100644 --- a/agent_api_rest/src/holder/mod.rs +++ b/agent_api_rest/src/holder/mod.rs @@ -28,6 +28,6 @@ pub fn router(holder_state: HolderState) -> Router { .route("/holder/offers/:offer_id/accept", post(accept)) .route("/holder/offers/:offer_id/reject", post(reject)), ) - .route("/openid4vci/offers", get(openid4vci::offers)) + .route("/openid4vci/offers", post(openid4vci::offers)) .with_state(holder_state) } From 76f96fe5147ee7c4dcb1765a6982b8f5939e2fdc Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Tue, 15 Oct 2024 18:36:31 +0200 Subject: [PATCH 56/68] test: add `services/:service_id` endpoints to Postman collection --- .../postman/ssi-agent.postman_collection.json | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/agent_api_rest/postman/ssi-agent.postman_collection.json b/agent_api_rest/postman/ssi-agent.postman_collection.json index 2cb20fdf..58d5008a 100644 --- a/agent_api_rest/postman/ssi-agent.postman_collection.json +++ b/agent_api_rest/postman/ssi-agent.postman_collection.json @@ -994,6 +994,38 @@ }, { "name": "services", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const jsonData = JSON.parse(responseBody);", + "", + "if (Array.isArray(jsonData) && jsonData.length > 0) {", + " const firstItem = jsonData[0];", + "", + " if (firstItem && typeof firstItem === 'object') {", + " const serviceId = firstItem.service_id;", + "", + " if (serviceId) {", + " pm.collectionVariables.set(\"SERVICE_ID\", serviceId);", + " }", + " }", + "}", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [], + "type": "text/javascript" + } + } + ], "request": { "method": "GET", "header": [], @@ -1009,6 +1041,44 @@ } }, "response": [] + }, + { + "name": "Service by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/v0/services/{{SERVICE_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "services", + "{{SERVICE_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "Linked Verifiable Presentation Service", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/v0/services/linked-verifiable-presentation-service", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "services", + "linked-verifiable-presentation-service" + ] + } + }, + "response": [] } ] } From dbbc5bafe6e4f4824b64445db8cdeff110966e3d Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Tue, 15 Oct 2024 18:47:26 +0200 Subject: [PATCH 57/68] fix: change method from `get` to `post` --- agent_issuance/src/offer/aggregate.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent_issuance/src/offer/aggregate.rs b/agent_issuance/src/offer/aggregate.rs index 3229b808..68937880 100644 --- a/agent_issuance/src/offer/aggregate.rs +++ b/agent_issuance/src/offer/aggregate.rs @@ -104,7 +104,7 @@ impl Aggregate for Offer { let client = reqwest::Client::new(); client - .get(target_url.clone()) + .post(target_url.clone()) .json(self.credential_offer.as_ref().ok_or(MissingCredentialOfferError)?) .send() .await From 82f0562137df360776ae155858251c7ead52cf51 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Wed, 16 Oct 2024 00:35:08 +0200 Subject: [PATCH 58/68] feat: add public `/linked-verifiable-presentations` endpoint --- agent_api_rest/src/holder/mod.rs | 4 ++++ agent_identity/src/service/aggregate.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/agent_api_rest/src/holder/mod.rs b/agent_api_rest/src/holder/mod.rs index 6ffc2d7d..dcc59842 100644 --- a/agent_api_rest/src/holder/mod.rs +++ b/agent_api_rest/src/holder/mod.rs @@ -29,5 +29,9 @@ pub fn router(holder_state: HolderState) -> Router { .route("/holder/offers/:offer_id/reject", post(reject)), ) .route("/openid4vci/offers", post(openid4vci::offers)) + .route( + "/linked-verifiable-presentations/:presentation_id", + get(presentation_signed), + ) .with_state(holder_state) } diff --git a/agent_identity/src/service/aggregate.rs b/agent_identity/src/service/aggregate.rs index d10a2327..c9a1af45 100644 --- a/agent_identity/src/service/aggregate.rs +++ b/agent_identity/src/service/aggregate.rs @@ -190,7 +190,7 @@ impl Aggregate for Service { .into_iter() .map(|presentation_id| { // TODO: Find a better way to construct the URL - format!("{origin}v0/holder/presentations/{presentation_id}/signed") + format!("{origin}linked-verifiable-presentations/{presentation_id}") .parse::() }) .collect::, _>>() From 3feb8075301173f3c05e1bcb3903f45f976949d1 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Thu, 17 Oct 2024 07:47:17 +0200 Subject: [PATCH 59/68] feat: add `offers_params` endpoint handler --- agent_api_rest/src/holder/mod.rs | 4 ++ agent_api_rest/src/holder/openid4vci/mod.rs | 57 ++++++++++++++++++++- agent_issuance/src/offer/aggregate.rs | 14 ++++- 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/agent_api_rest/src/holder/mod.rs b/agent_api_rest/src/holder/mod.rs index dcc59842..62c045e8 100644 --- a/agent_api_rest/src/holder/mod.rs +++ b/agent_api_rest/src/holder/mod.rs @@ -28,7 +28,11 @@ pub fn router(holder_state: HolderState) -> Router { .route("/holder/offers/:offer_id/accept", post(accept)) .route("/holder/offers/:offer_id/reject", post(reject)), ) + // TODO: move behind UniCore API .route("/openid4vci/offers", post(openid4vci::offers)) + // TODO: move behind UniCore API + .route("/credential_offer", get(openid4vci::offers_params)) + .route("/", get(openid4vci::offers_params)) .route( "/linked-verifiable-presentations/:presentation_id", get(presentation_signed), diff --git a/agent_api_rest/src/holder/openid4vci/mod.rs b/agent_api_rest/src/holder/openid4vci/mod.rs index 95145b61..bbb9b027 100644 --- a/agent_api_rest/src/holder/openid4vci/mod.rs +++ b/agent_api_rest/src/holder/openid4vci/mod.rs @@ -1,13 +1,14 @@ use agent_holder::{offer::command::OfferCommand, state::HolderState}; -use agent_shared::handlers::command_handler; +use agent_shared::handlers::{command_handler, query_handler}; use axum::{ extract::State, response::{IntoResponse, Response}, - Json, + Form, Json, }; use hyper::StatusCode; use oid4vci::credential_offer::CredentialOffer; use serde::{Deserialize, Serialize}; +use serde_json::Value; use tracing::info; #[derive(Deserialize, Serialize)] @@ -39,3 +40,55 @@ pub(crate) async fn offers(State(state): State, Json(payload): Json Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } } + +#[axum_macros::debug_handler] +pub(crate) async fn offers_params( + State(state): State, + Form(payload): Form, +) -> Response { + offers_inner(state, payload).await +} + +pub(crate) async fn offers_inner(state: HolderState, payload: serde_json::Value) -> Response { + info!("Request Body: {}", payload); + + let credential_offer_result: Result = + if let Some(credential_offer) = payload.get("credential_offer").and_then(Value::as_str) { + format!("openid-credential-offer://?credential_offer={credential_offer}") + } else if let Some(credential_offer_uri) = payload.get("credential_offer_uri").and_then(Value::as_str) { + format!("openid-credential-offer://?credential_offer_uri={credential_offer_uri}") + } else { + return (StatusCode::BAD_REQUEST, "invalid payload").into_response(); + } + .parse(); + + let credential_offer = match credential_offer_result { + Ok(credential_offer) => credential_offer, + Err(_) => return (StatusCode::BAD_REQUEST, "invalid payload").into_response(), + }; + + let received_offer_id = uuid::Uuid::new_v4().to_string(); + + info!("Credential Offer: {:#?}", credential_offer); + + let command = OfferCommand::ReceiveCredentialOffer { + offer_id: received_offer_id.clone(), + credential_offer, + }; + + // Add the Credential Offer to the state. + if command_handler(&received_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(); + } + + match query_handler(&received_offer_id, &state.query.received_offer).await { + // TODO: add Location header + Ok(Some(received_offer)) => (StatusCode::CREATED, Json(received_offer)).into_response(), + _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } +} diff --git a/agent_issuance/src/offer/aggregate.rs b/agent_issuance/src/offer/aggregate.rs index 68937880..8105ab4e 100644 --- a/agent_issuance/src/offer/aggregate.rs +++ b/agent_issuance/src/offer/aggregate.rs @@ -103,9 +103,19 @@ impl Aggregate for Offer { // TODO: add to `service`? let client = reqwest::Client::new(); + let form_url_encoded_credential_offer = self + .credential_offer + .as_ref() + .ok_or(MissingCredentialOfferError)? + .to_string(); + + let target = + form_url_encoded_credential_offer.replace("openid-credential-offer://", target_url.as_str()); + + info!("Sending credential offer to: {}", target); + client - .post(target_url.clone()) - .json(self.credential_offer.as_ref().ok_or(MissingCredentialOfferError)?) + .get(target) .send() .await .map_err(|e| SendCredentialOfferError(e.to_string()))?; From b56c6dd1a2708f5ad5cfc0c40b9dd3e8001f519c Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Tue, 19 Nov 2024 15:07:04 +0100 Subject: [PATCH 60/68] style: make requests human-readable --- .../postman/ssi-agent.postman_collection.json | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/agent_api_rest/postman/ssi-agent.postman_collection.json b/agent_api_rest/postman/ssi-agent.postman_collection.json index 58d5008a..8c5a5d86 100644 --- a/agent_api_rest/postman/ssi-agent.postman_collection.json +++ b/agent_api_rest/postman/ssi-agent.postman_collection.json @@ -275,7 +275,7 @@ "response": [] }, { - "name": "all_offers", + "name": "All Offers", "event": [ { "listen": "test", @@ -315,13 +315,13 @@ "response": [] }, { - "name": "offers_send", + "name": "Send Offer", "request": { "method": "POST", "header": [], "body": { "mode": "raw", - "raw": "{\n \"offerId\": \"{{OFFER_ID}}\",\n \"targetUrl\": \"{{HOST}}/openid4vci/offers\"\n}", + "raw": "{\n \"offerId\": \"{{OFFER_ID}}\",\n \"targetUrl\": \"{{HOST}}\"\n}", "options": { "raw": { "language": "json" @@ -763,7 +763,7 @@ "name": "Holder", "item": [ { - "name": "offers", + "name": "All Received Offers", "event": [ { "listen": "test", @@ -812,7 +812,7 @@ "response": [] }, { - "name": "offers_accept", + "name": "Accept Received Offer", "request": { "method": "POST", "header": [], @@ -833,7 +833,7 @@ "response": [] }, { - "name": "offers_reject", + "name": "Reject Received Offer", "request": { "method": "POST", "header": [], @@ -854,7 +854,7 @@ "response": [] }, { - "name": "credentials", + "name": "All Holder Credentials", "event": [ { "listen": "test", @@ -893,7 +893,7 @@ "response": [] }, { - "name": "presentations", + "name": "All Presentations", "event": [ { "listen": "test", @@ -932,7 +932,7 @@ "response": [] }, { - "name": "presentations", + "name": "Create new Presentation", "request": { "method": "POST", "header": [], @@ -965,7 +965,7 @@ "name": "Identity", "item": [ { - "name": "services/linked-vp", + "name": "Create new Linked Verifiable Presentation Service", "request": { "method": "POST", "header": [], @@ -993,7 +993,7 @@ "response": [] }, { - "name": "services", + "name": "All Services", "event": [ { "listen": "test", From 215e030450bf17d3df401c0709abc3a8ccfc5a1e Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Tue, 19 Nov 2024 15:12:49 +0100 Subject: [PATCH 61/68] test: update path of linked VP in test --- agent_identity/src/service/aggregate.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent_identity/src/service/aggregate.rs b/agent_identity/src/service/aggregate.rs index c9a1af45..b11b7647 100644 --- a/agent_identity/src/service/aggregate.rs +++ b/agent_identity/src/service/aggregate.rs @@ -333,7 +333,7 @@ pub mod test_utils { ) .type_("LinkedVerifiablePresentation") .service_endpoint(ServiceEndpoint::from(OrderedSet::from_iter(vec![format!( - "{origin}/v0/holder/presentations/presentation-1/signed" + "{origin}/linked-verifiable-presentations/presentation-1" ) .parse::() .unwrap()]))) From 578c917f3cce3082e6ebb6570f861b2afcd460c9 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Thu, 21 Nov 2024 10:28:15 +0100 Subject: [PATCH 62/68] fix: update `did_configuration` comment --- agent_api_rest/src/identity/well_known/did_configuration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent_api_rest/src/identity/well_known/did_configuration.rs b/agent_api_rest/src/identity/well_known/did_configuration.rs index d246028a..a70ed6e3 100644 --- a/agent_api_rest/src/identity/well_known/did_configuration.rs +++ b/agent_api_rest/src/identity/well_known/did_configuration.rs @@ -12,7 +12,7 @@ use hyper::StatusCode; #[axum_macros::debug_handler] pub(crate) async fn did_configuration(State(state): State) -> Response { - // Get the DomainLinkageConfiguration if it exists. + // Get the DID Configuration Resource if it exists. match query_handler(DOMAIN_LINKAGE_SERVICE_ID, &state.query.service).await { Ok(Some(ServiceView { resource: Some(ServiceResource::DomainLinkage(domain_linkage_configuration)), From 55b208b468a0a77d5d96dc6e5a15b38d6548185c Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Thu, 21 Nov 2024 10:30:38 +0100 Subject: [PATCH 63/68] fix: use ACME Corp --- agent_api_rest/postman/ssi-agent.postman_collection.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agent_api_rest/postman/ssi-agent.postman_collection.json b/agent_api_rest/postman/ssi-agent.postman_collection.json index 8c5a5d86..8e134011 100644 --- a/agent_api_rest/postman/ssi-agent.postman_collection.json +++ b/agent_api_rest/postman/ssi-agent.postman_collection.json @@ -108,7 +108,7 @@ "response": [] }, { - "name": "Thuiswinkel Waarborg Credential", + "name": "ACME Corp Credential", "event": [ { "listen": "test", @@ -141,7 +141,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"offerId\":\"{{OFFER_ID}}\",\n \"credentialConfigurationId\": \"w3c_vc_credential\",\n \"credential\": {\n \"credentialSubject\": {\n \"id\": \"https://ecommerce.impierce.com/\",\n \"image\": \"https://www.thuiswinkel.org/Images/logo-thuiswinkel_waarborg.svg\",\n \"name\": \"VirtualVendors\",\n \"certificaat\": {\n \"type\": \"ThuiswinkelWaarborg\",\n \"certificeringsDatum\": \"2024-06-26\",\n \"geldigheidsPeriode\": \"1 jaar\",\n \"garanties\": [\n \"Het bedrijf is echt en bereikbaar.\",\n \"Voldoet aan de Thuiswinkel Algemene Voorwaarden.\",\n \"14 dagen bedenktijd.\",\n \"Veilige betaalmethoden.\",\n \"Duidelijke product/servicebeschrijvingen.\",\n \"Transparant bestelproces.\",\n \"Duidelijke prijzen.\",\n \"Veilige betaalomgeving.\",\n \"Veilige omgang met persoonlijke gegevens.\",\n \"Effectieve klachtenafhandeling en onafhankelijke geschillenbemiddeling.\"\n ]\n }\n }\n }\n}", + "raw": "{\n \"offerId\":\"{{OFFER_ID}}\",\n \"credentialConfigurationId\": \"w3c_vc_credential\",\n \"credential\": {\n \"credentialSubject\": {\n \"id\": \"https://ecommerce.impierce.com/\",\n \"image\": \"https://static.wikia.nocookie.net/fictionalcompanies/images/c/c2/ACME_Corporation.png\",\n \"name\": \"VirtualVendors\",\n \"certificaat\": {\n \"type\": \"ACMECorpCredential\",\n \"certificeringsDatum\": \"2024-06-26\",\n \"geldigheidsPeriode\": \"1 jaar\",\n \"garanties\": [\n \"Het bedrijf is echt en bereikbaar.\",\n \"Voldoet aan de Thuiswinkel Algemene Voorwaarden.\",\n \"14 dagen bedenktijd.\",\n \"Veilige betaalmethoden.\",\n \"Duidelijke product/servicebeschrijvingen.\",\n \"Transparant bestelproces.\",\n \"Duidelijke prijzen.\",\n \"Veilige betaalomgeving.\",\n \"Veilige omgang met persoonlijke gegevens.\",\n \"Effectieve klachtenafhandeling en onafhankelijke geschillenbemiddeling.\"\n ]\n }\n }\n }\n}", "options": { "raw": { "language": "json" From 0d0efc7ec334e36cd2a1662aa17e20670d561fb4 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Mon, 25 Nov 2024 13:47:19 +0100 Subject: [PATCH 64/68] fix: improve error message --- agent_holder/src/offer/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent_holder/src/offer/error.rs b/agent_holder/src/offer/error.rs index 51858c1a..f65f0539 100644 --- a/agent_holder/src/offer/error.rs +++ b/agent_holder/src/offer/error.rs @@ -32,6 +32,6 @@ pub enum OfferError { UnsupportedDeferredCredentialResponseError, #[error("Batch Credential Request are not supported")] BatchCredentialRequestError, - #[error("Non-JWT credentials are not supported")] + #[error("Only JWT credentials are supported")] UnsupportedCredentialFormatError, } From ec50ddb6e885a3a6c18a0b5f613f93260b6c44b3 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Mon, 25 Nov 2024 14:00:50 +0100 Subject: [PATCH 65/68] fix: add comment --- agent_holder/src/presentation/aggregate.rs | 1 + agent_identity/src/service/aggregate.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/agent_holder/src/presentation/aggregate.rs b/agent_holder/src/presentation/aggregate.rs index ffb07230..bcd2a167 100644 --- a/agent_holder/src/presentation/aggregate.rs +++ b/agent_holder/src/presentation/aggregate.rs @@ -78,6 +78,7 @@ impl Aggregate for Presentation { let header = Header { alg: get_preferred_signing_algorithm(), typ: Some("JWT".to_string()), + // TODO: make dynamic kid: Some(format!("{subject_did}#key-0")), ..Default::default() }; diff --git a/agent_identity/src/service/aggregate.rs b/agent_identity/src/service/aggregate.rs index b11b7647..2e35199e 100644 --- a/agent_identity/src/service/aggregate.rs +++ b/agent_identity/src/service/aggregate.rs @@ -79,6 +79,7 @@ impl Aggregate for Service { let (issuance_date, expiration_date) = { let issuance_date = Timestamp::now_utc(); let expiration_date = issuance_date + // TODO: make this configurable .checked_add(Duration::days(365)) .ok_or(InvalidTimestampError)?; From 0e3e2181b506a6e479949ff4eb482757cfcf5ff3 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Mon, 25 Nov 2024 15:28:08 +0100 Subject: [PATCH 66/68] fix: remove `/openid4vci/offers` endpoint --- agent_api_rest/src/holder/mod.rs | 4 +-- agent_api_rest/src/holder/openid4vci/mod.rs | 31 --------------------- 2 files changed, 1 insertion(+), 34 deletions(-) diff --git a/agent_api_rest/src/holder/mod.rs b/agent_api_rest/src/holder/mod.rs index 62c045e8..97f8a7fd 100644 --- a/agent_api_rest/src/holder/mod.rs +++ b/agent_api_rest/src/holder/mod.rs @@ -28,9 +28,7 @@ pub fn router(holder_state: HolderState) -> Router { .route("/holder/offers/:offer_id/accept", post(accept)) .route("/holder/offers/:offer_id/reject", post(reject)), ) - // TODO: move behind UniCore API - .route("/openid4vci/offers", post(openid4vci::offers)) - // TODO: move behind UniCore API + // TODO: move these behind some sort of authentication? .route("/credential_offer", get(openid4vci::offers_params)) .route("/", get(openid4vci::offers_params)) .route( diff --git a/agent_api_rest/src/holder/openid4vci/mod.rs b/agent_api_rest/src/holder/openid4vci/mod.rs index bbb9b027..6cabf28a 100644 --- a/agent_api_rest/src/holder/openid4vci/mod.rs +++ b/agent_api_rest/src/holder/openid4vci/mod.rs @@ -7,40 +7,9 @@ use axum::{ }; use hyper::StatusCode; use oid4vci::credential_offer::CredentialOffer; -use serde::{Deserialize, Serialize}; use serde_json::Value; use tracing::info; -#[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 = 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(), - } -} - #[axum_macros::debug_handler] pub(crate) async fn offers_params( State(state): State, From 5813d44eddb6e02a2c3083b6941bce89d50d71b2 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Mon, 25 Nov 2024 15:28:57 +0100 Subject: [PATCH 67/68] docs: add aggregate fields --- agent_holder/src/presentation/README.md | 2 ++ agent_identity/src/document/README.md | 1 + agent_identity/src/service/README.md | 4 ++++ 3 files changed, 7 insertions(+) diff --git a/agent_holder/src/presentation/README.md b/agent_holder/src/presentation/README.md index 562e6ae0..dc37302b 100644 --- a/agent_holder/src/presentation/README.md +++ b/agent_holder/src/presentation/README.md @@ -1,3 +1,5 @@ # Presentation This aggregate holds everything related to a presentation: +- presentation_id +- signed (the signed Presentation) diff --git a/agent_identity/src/document/README.md b/agent_identity/src/document/README.md index 95ac6726..5f057d11 100644 --- a/agent_identity/src/document/README.md +++ b/agent_identity/src/document/README.md @@ -1,3 +1,4 @@ # Document This aggregate holds everything related to a document: + - document (DID dcoument) diff --git a/agent_identity/src/service/README.md b/agent_identity/src/service/README.md index cd8d147a..3af85459 100644 --- a/agent_identity/src/service/README.md +++ b/agent_identity/src/service/README.md @@ -1,3 +1,7 @@ # Service This aggregate holds everything related to a service: +- id +- service (DID Document Service) +- resource (e.g. Domain Linkage resource) + From 566e207c0d3b7c7db7fd9734164f7dda432c224b Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Mon, 25 Nov 2024 15:40:14 +0100 Subject: [PATCH 68/68] fix: clarify commented env variables --- .env.example | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 7cfdf006..9602d06e 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,12 @@ UNICORE__LOG_FORMAT=text UNICORE__EVENT_STORE__TYPE=in_memory -# UNICORE__EVENT_STORE__CONNECTION_STRING="postgresql://demo_user:demo_pass@cqrs-postgres-db:5432/demo" UNICORE__URL="http://localhost:3033" UNICORE__SECRET_MANAGER__STRONGHOLD_PATH="agent_secret_manager/tests/res/temp.stronghold" -# UNICORE__SECRET_MANAGER__STRONGHOLD_PATH="agent_secret_manager/tests/res/test.stronghold" UNICORE__SECRET_MANAGER__STRONGHOLD_PASSWORD="secure_password" -# UNICORE__SECRET_MANAGER__ISSUER_KEY_ID="9O66nzWqYYy1LmmiOudOlh2SMIaUWoTS" +# Uncomment to enable DID IOTA for testing purposes +# UNICORE__SECRET_MANAGER__STRONGHOLD_PATH="agent_secret_manager/tests/res/test.stronghold" +# UNICORE__SECRET_MANAGER__ISSUER_EDDSA_KEY_ID="9O66nzWqYYy1LmmiOudOlh2SMIaUWoTS" # UNICORE__SECRET_MANAGER__ISSUER_DID="did:iota:rms:0x42ad588322e58b3c07aa39e4948d021ee17ecb5747915e9e1f35f028d7ecaf90" # UNICORE__SECRET_MANAGER__ISSUER_FRAGMENT="bQKQRzaop7CgEvqVq8UlgLGsdF-R-hnLFkKFZqW2VN0"