From d00354a30faedbfd79d680bff603b168219dbf0d Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Wed, 21 Aug 2024 10:51:46 +0200 Subject: [PATCH 01/48] WIP --- Cargo.lock | 63 +++++++++++++++++++ agent_api_rest/Cargo.toml | 2 + .../issuance/credential_issuer/credential.rs | 11 ++++ .../src/issuance/credential_issuer/mod.rs | 7 +++ .../credential_issuer/well_known/mod.rs | 7 +++ .../well_known/oauth_authorization_server.rs | 11 ++++ agent_api_rest/src/lib.rs | 33 +++++++++- 7 files changed, 133 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 1f42cbae..335ed420 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,6 +95,8 @@ dependencies = [ "tracing-subscriber", "tracing-test", "url", + "utoipa 5.0.0-alpha.1", + "utoipa-scalar", "uuid", "wiremock", ] @@ -8056,6 +8058,67 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "utoipa" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23" +dependencies = [ + "indexmap 2.2.6", + "serde", + "serde_json", + "utoipa-gen 4.3.0", +] + +[[package]] +name = "utoipa" +version = "5.0.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c082de846a4d434a9dcfe3358dbe4a0aa5d4f826c3af29cdbd97404e1ffe71f4" +dependencies = [ + "indexmap 2.2.6", + "serde", + "serde_json", + "serde_yaml", + "utoipa-gen 5.0.0-alpha.1", +] + +[[package]] +name = "utoipa-gen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bf0e16c02bc4bf5322ab65f10ab1149bdbcaa782cba66dc7057370a3f8190be" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.67", +] + +[[package]] +name = "utoipa-gen" +version = "5.0.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a71c23e17df16027cc552b5b249a2a5e6a1ea36ab37363a1ac29b69ab36035" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.67", +] + +[[package]] +name = "utoipa-scalar" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ab4b7269d14d93626b0bfedf212f1b0995cb7d13d35daba21d579511e7fae8" +dependencies = [ + "axum 0.7.5", + "serde", + "serde_json", + "utoipa 4.2.3", +] + [[package]] name = "uuid" version = "1.8.0" diff --git a/agent_api_rest/Cargo.toml b/agent_api_rest/Cargo.toml index 88210ade..a2b54517 100644 --- a/agent_api_rest/Cargo.toml +++ b/agent_api_rest/Cargo.toml @@ -25,6 +25,8 @@ tower-http.workspace = true tracing.workspace = true tracing-subscriber.workspace = true uuid.workspace = true +utoipa = { version = "=5.0.0-alpha.1", features = ["axum_extras", "yaml"] } +utoipa-scalar = { version = "=0.1.0", features = ["axum"] } [dev-dependencies] agent_event_publisher_http = { path = "../agent_event_publisher_http", 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..a1477122 100644 --- a/agent_api_rest/src/issuance/credential_issuer/credential.rs +++ b/agent_api_rest/src/issuance/credential_issuer/credential.rs @@ -27,6 +27,17 @@ use tracing::{error, info}; const DEFAULT_EXTERNAL_SERVER_RESPONSE_TIMEOUT_MS: u64 = 1000; const POLLING_INTERVAL_MS: u64 = 100; +/// Credential endpoint +/// +/// Do something with credentials +#[utoipa::path( + get, + path = "/credential", + tag = "credential", + responses( + (status = 200, description = "List all credentials successfully", body = [CredentialRequest]) + ) +)] #[axum_macros::debug_handler] pub(crate) async fn credential( State(state): State, diff --git a/agent_api_rest/src/issuance/credential_issuer/mod.rs b/agent_api_rest/src/issuance/credential_issuer/mod.rs index df6bc6fe..97c53c6d 100644 --- a/agent_api_rest/src/issuance/credential_issuer/mod.rs +++ b/agent_api_rest/src/issuance/credential_issuer/mod.rs @@ -1,3 +1,10 @@ pub mod credential; pub mod token; pub mod well_known; + +#[derive(utoipa::OpenApi)] +#[openapi( + paths(crate::issuance::credential_issuer::credential::credential), + // components(schemas(Todo, TodoError)) +)] +pub(crate) struct CredentialApi; diff --git a/agent_api_rest/src/issuance/credential_issuer/well_known/mod.rs b/agent_api_rest/src/issuance/credential_issuer/well_known/mod.rs index 3e7935ad..65cac477 100644 --- a/agent_api_rest/src/issuance/credential_issuer/well_known/mod.rs +++ b/agent_api_rest/src/issuance/credential_issuer/well_known/mod.rs @@ -1,2 +1,9 @@ pub mod oauth_authorization_server; pub mod openid_credential_issuer; + +#[derive(utoipa::OpenApi)] +#[openapi( + paths(crate::issuance::credential_issuer::well_known::oauth_authorization_server::oauth_authorization_server), + // components(schemas(Todo, TodoError)) +)] +pub(crate) struct WellKnownApi; 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..fd763ae0 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 @@ -9,6 +9,17 @@ use axum::{ response::{IntoResponse, Response}, }; +/// List all Todo items +/// +/// List all Todo items from in-memory storage. +#[utoipa::path( + get, + path = "/oauth-authorization-server", + tag = "todo", + responses( + (status = 200, description = "List all todos successfully", body = [Todo]) + ) +)] #[axum_macros::debug_handler] pub(crate) async fn oauth_authorization_server(State(state): State) -> Response { match query_handler(SERVER_CONFIG_ID, &state.query.server_config).await { diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index d2d72f1d..49810b02 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -23,6 +23,8 @@ use issuance::credentials::{credentials, get_credentials}; use issuance::offers::offers; use tower_http::trace::TraceLayer; use tracing::{info_span, Span}; +use utoipa::OpenApi; +use utoipa_scalar::{Scalar, Servable}; use verification::{ authorization_requests::{authorization_requests, get_authorization_requests}, relying_party::{redirect::redirect, request::request}, @@ -43,6 +45,8 @@ pub fn app(state: ApplicationState) -> Router { } }; + Router::new().merge(Scalar::with_url("/scalar", ApiDoc::openapi())); + Router::new() .nest( &path(API_VERSION), @@ -122,6 +126,26 @@ fn get_base_path() -> Result { }) } +// #[derive(OpenApi)] +// #[openapi(modifiers(), nest((path = "/v0/todos", api = WellKnownApi)), tags((name = "well-known")))] +// struct ApiDoc; + +#[derive(utoipa::OpenApi)] +#[openapi( + // modifiers(), + nest( + (path = "/.well-known", api = crate::issuance::credential_issuer::well_known::WellKnownApi), + (path = "/v0/credential", api = crate::issuance::credential_issuer::CredentialApi), + ), + // paths( + // crate::issuance::credential_issuer::CredentialApi + // ), + tags( + (name = "todo", description = "Todo items management API") + ) + )] +pub struct ApiDoc; + #[cfg(test)] mod tests { use std::collections::HashMap; @@ -135,8 +159,9 @@ mod tests { credential_issuer_metadata::CredentialIssuerMetadata, }; use serde_json::json; + use utoipa::OpenApi; - use crate::app; + use crate::{app, ApiDoc}; pub const CREDENTIAL_CONFIGURATION_ID: &str = "badge"; pub const OFFER_ID: &str = "00000000-0000-0000-0000-000000000000"; @@ -180,6 +205,12 @@ mod tests { async fn handler() {} + #[tokio::test] + async fn openapi() { + let yaml = serde_yaml::to_string(&ApiDoc::openapi()).unwrap(); + println!("{}", yaml); + } + #[tokio::test] #[should_panic] async fn test_base_path_routes() { From 4ffa6d10cfcd1f54cd59035bb9d441f77f154678 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Thu, 22 Aug 2024 10:28:16 +0200 Subject: [PATCH 02/48] 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 375110eb4ef5614ca64fe5a22fbdc9ac2e26c6e3 Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Sun, 25 Aug 2024 00:10:34 +0200 Subject: [PATCH 03/48] refactor: add `openapi` module, fix tags, document more endpoints --- Cargo.lock | 115 +++++++++---- agent_api_rest/Cargo.toml | 25 ++- agent_api_rest/openapi.yaml | 159 ++++++++++++------ .../issuance/credential_issuer/credential.rs | 15 +- .../src/issuance/credential_issuer/mod.rs | 7 - .../credential_issuer/well_known/mod.rs | 7 - .../well_known/oauth_authorization_server.rs | 8 +- .../well_known/openid_credential_issuer.rs | 11 ++ agent_api_rest/src/issuance/credentials.rs | 29 +++- agent_api_rest/src/issuance/mod.rs | 1 + agent_api_rest/src/issuance/offers.rs | 15 +- agent_api_rest/src/issuance/openapi.rs | 29 ++++ agent_api_rest/src/lib.rs | 17 +- .../verification/authorization_requests.rs | 28 ++- agent_api_rest/test.openapi.yaml | 151 +++++++++++++++++ 15 files changed, 492 insertions(+), 125 deletions(-) create mode 100644 agent_api_rest/src/issuance/openapi.rs create mode 100644 agent_api_rest/test.openapi.yaml diff --git a/Cargo.lock b/Cargo.lock index 335ed420..d91dd211 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,8 +95,9 @@ dependencies = [ "tracing-subscriber", "tracing-test", "url", - "utoipa 5.0.0-alpha.1", + "utoipa", "utoipa-scalar", + "utoipa-swagger-ui", "uuid", "wiremock", ] @@ -3114,7 +3115,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.7", + "socket2 0.4.10", "tokio", "tower-service", "tracing", @@ -5714,6 +5715,7 @@ checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ "base64 0.22.1", "bytes", + "futures-channel", "futures-core", "futures-util", "http 1.1.0", @@ -5975,6 +5977,40 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rust-embed" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.67", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" +dependencies = [ + "sha2 0.10.8", + "walkdir", +] + [[package]] name = "rust-ini" version = "0.19.0" @@ -6157,6 +6193,15 @@ dependencies = [ "cipher", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scc" version = "2.1.1" @@ -8060,63 +8105,53 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utoipa" -version = "4.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23" -dependencies = [ - "indexmap 2.2.6", - "serde", - "serde_json", - "utoipa-gen 4.3.0", -] - -[[package]] -name = "utoipa" -version = "5.0.0-alpha.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c082de846a4d434a9dcfe3358dbe4a0aa5d4f826c3af29cdbd97404e1ffe71f4" +version = "5.0.0-alpha.2" +source = "git+https://github.com/juhaku/utoipa.git?rev=f2a7143#f2a7143fd92767046abc9433daedc15938afe1a2" dependencies = [ "indexmap 2.2.6", "serde", "serde_json", "serde_yaml", - "utoipa-gen 5.0.0-alpha.1", + "utoipa-gen", ] [[package]] name = "utoipa-gen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bf0e16c02bc4bf5322ab65f10ab1149bdbcaa782cba66dc7057370a3f8190be" +version = "5.0.0-alpha.2" +source = "git+https://github.com/juhaku/utoipa.git?rev=f2a7143#f2a7143fd92767046abc9433daedc15938afe1a2" dependencies = [ - "proc-macro-error", "proc-macro2", "quote", + "regex", "syn 2.0.67", ] [[package]] -name = "utoipa-gen" -version = "5.0.0-alpha.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a71c23e17df16027cc552b5b249a2a5e6a1ea36ab37363a1ac29b69ab36035" +name = "utoipa-scalar" +version = "0.2.0-alpha.0" +source = "git+https://github.com/juhaku/utoipa.git?rev=f2a7143#f2a7143fd92767046abc9433daedc15938afe1a2" dependencies = [ - "proc-macro2", - "quote", - "regex", - "syn 2.0.67", + "axum 0.7.5", + "serde", + "serde_json", + "utoipa", ] [[package]] -name = "utoipa-scalar" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3ab4b7269d14d93626b0bfedf212f1b0995cb7d13d35daba21d579511e7fae8" +name = "utoipa-swagger-ui" +version = "7.1.1-alpha.2" +source = "git+https://github.com/juhaku/utoipa.git?rev=f2a7143#f2a7143fd92767046abc9433daedc15938afe1a2" dependencies = [ "axum 0.7.5", + "mime_guess", + "regex", + "reqwest 0.12.5", + "rust-embed", "serde", "serde_json", - "utoipa 4.2.3", + "url", + "utoipa", + "zip", ] [[package]] @@ -8160,6 +8195,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" diff --git a/agent_api_rest/Cargo.toml b/agent_api_rest/Cargo.toml index a2b54517..2cbc3f85 100644 --- a/agent_api_rest/Cargo.toml +++ b/agent_api_rest/Cargo.toml @@ -25,16 +25,33 @@ tower-http.workspace = true tracing.workspace = true tracing-subscriber.workspace = true uuid.workspace = true -utoipa = { version = "=5.0.0-alpha.1", features = ["axum_extras", "yaml"] } -utoipa-scalar = { version = "=0.1.0", features = ["axum"] } +# utoipa = { version = "=5.0.0-alpha.1", features = ["axum_extras", "yaml"] } +# utoipa-scalar = { version = "=0.2.0-alpha.0", features = ["axum"] } +# TODO: wait for new release that contains PR juhaku/utoipa#1002 (current version `=5.0.0-alpha.1`) +utoipa = { git = "https://github.com/juhaku/utoipa.git", rev = "f2a7143", features = [ + "axum_extras", + "yaml", +] } +# TODO: wait for new release that contains PR juhaku/utoipa#1002 (current version `=5.0.0-alpha.1`) +utoipa-scalar = { git = "https://github.com/juhaku/utoipa.git", rev = "f2a7143", features = [ + "axum", +] } +# TODO: wait for new release that contains PR juhaku/utoipa#1002 (current version `=5.0.0-alpha.1`) +utoipa-swagger-ui = { git = "https://github.com/juhaku/utoipa.git", rev = "f2a7143", features = [ + "axum", +] } [dev-dependencies] -agent_event_publisher_http = { path = "../agent_event_publisher_http", features = ["test_utils"] } +agent_event_publisher_http = { path = "../agent_event_publisher_http", 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"] } agent_store = { path = "../agent_store" } -agent_verification = { path = "../agent_verification", features = ["test_utils"] } +agent_verification = { path = "../agent_verification", features = [ + "test_utils", +] } futures.workspace = true jsonwebtoken.workspace = true diff --git a/agent_api_rest/openapi.yaml b/agent_api_rest/openapi.yaml index 37b3fe35..b2f96e73 100644 --- a/agent_api_rest/openapi.yaml +++ b/agent_api_rest/openapi.yaml @@ -1,4 +1,4 @@ -openapi: 3.0.3 +openapi: 3.1.0 info: title: SSI Agent - REST API description: A lightweight REST API for the SSI Agent @@ -12,7 +12,7 @@ paths: /v0/configurations/credential_configurations: post: tags: - - Configurations + - Configurations summary: Create a new Credential Configuration # description: n/a requestBody: @@ -36,7 +36,7 @@ paths: type: string example: VerifiableCredential example: - - VerifiableCredential + - VerifiableCredential display: type: array items: @@ -58,11 +58,11 @@ paths: type: string example: Identity Credential example: - - locale: en - logo: - alt_text: UniCore Logo - url: https://impierce.com/images/logo-blue.png - name: Identity Credential + - locale: en + logo: + alt_text: UniCore Logo + url: https://impierce.com/images/logo-blue.png + name: Identity Credential required: - credentialConfigurationId - format @@ -74,14 +74,14 @@ paths: credentialConfigurationId: openbadge_credential credential_definition: type: - - VerifiableCredential - - OpenBadgeCredential + - VerifiableCredential + - OpenBadgeCredential display: - - locale: en - logo: - alt_text: UniCore Logo - url: https://impierce.com/images/logo-blue.png - name: Identity Credential + - locale: en + logo: + alt_text: UniCore Logo + url: https://impierce.com/images/logo-blue.png + name: Identity Credential format: jwt_vc_json w3c_vc_credential_configurations: summary: W3C VC Data Model @@ -89,16 +89,16 @@ paths: credentialConfigurationId: w3c_vc_credential credential_definition: type: - - VerifiableCredential + - VerifiableCredential display: - - locale: en - logo: - alt_text: UniCore Logo - url: https://impierce.com/images/logo-blue.png - name: Identity Credential + - locale: en + logo: + alt_text: UniCore Logo + url: https://impierce.com/images/logo-blue.png + name: Identity Credential format: jwt_vc_json responses: - '200': + "200": description: A Credential Configuration has been successfully added to the Credential Issuer Metadata /v0/credentials: @@ -152,9 +152,9 @@ paths: name: Teamwork type: Achievement type: - - AchievementSubject + - AchievementSubject credentialConfigurationId: openbadge_credential - offerId: '001' + offerId: "001" w3c-vc-dm: summary: W3C VC Data Model value: @@ -164,7 +164,7 @@ paths: first_name: Ferris last_name: Crabman credentialConfigurationId: w3c_vc_credential - offerId: '001' + offerId: "001" responses: "201": description: An Open Badge 3.0 has successfully been created for the provided credentialSubject @@ -183,12 +183,12 @@ paths: summary: Open Badges 3.0 value: "@context": - - https://www.w3.org/2018/credentials/v1 - - https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json + - 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 + - VerifiableCredential + - OpenBadgeCredential name: Identity Credential credentialSubject: type: @@ -197,30 +197,32 @@ paths: 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 + 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 + description: + This badge recognizes the development of the capacity to collaborate within a group environment. name: Teamwork issuer: id: http://192.168.1.127:3033 type: Profile name: UniCore - issuanceDate: '2024-06-21T12:34:54Z' + issuanceDate: "2024-06-21T12:34:54Z" w3c-vc-dm: summary: W3C VC Data Model value: "@context": https://www.w3.org/2018/credentials/v1 type: - - VerifiableCredential + - VerifiableCredential credentialSubject: - dob: '1982-01-01' + dob: "1982-01-01" first_name: Ferris last_name: Crabman issuer: id: http://192.168.1.127:3033/ name: UniCore - issuanceDate: '2024-06-21T12:43:20Z' + issuanceDate: "2024-06-21T12:43:20Z" /v0/credentials/{credential_id}: get: @@ -337,15 +339,15 @@ paths: presentation_definition: id: Verifiable Presentation request for sign-on input_descriptors: - - id: Request for Verifiable Credential - constraints: - fields: - - path: - - "$.vc.type" - filter: - type: array - contains: - const: OpenBadgeCredential + - id: Request for Verifiable Credential + constraints: + fields: + - path: + - "$.vc.type" + filter: + type: array + contains: + const: OpenBadgeCredential oid4vp-w3c-vc-dm: summary: OID4VP W3C VC Data Model value: @@ -353,15 +355,15 @@ paths: presentation_definition: id: Verifiable Presentation request for sign-on input_descriptors: - - id: Request for Verifiable Credential - constraints: - fields: - - path: - - "$.vc.type" - filter: - type: array - contains: - const: VerifiableCredential + - id: Request for Verifiable Credential + constraints: + fields: + - path: + - "$.vc.type" + filter: + type: array + contains: + const: VerifiableCredential responses: "201": description: An Authorization Request has successfully been created @@ -413,3 +415,54 @@ paths: summary: Standard OAuth 2.0 redirection endpoint tags: - (proxied) + + /v0/identity/offer: + post: + summary: Receive offers from third-parties + tags: + - Identity + requestBody: + content: + application/json: + schema: + type: object + examples: + receive-0: + summary: Example invitation + externalValue: "requests/payload.json" + + /v0/identity/offers: + get: + summary: List all current offers + tags: + - Identity + /v0/identity/offer/{offerId}/accept: + post: + summary: Accept an offer. UniCore will then make a request and accept the offer. + tags: + - Identity + parameters: + - in: path + name: offerId + schema: + type: integer + required: true + # description: Numeric ID of the user to get + /v0/identity/offer/{offerId}/decline: + post: + summary: Refuses an offer. UniCore will not make a request. + tags: + - Identity + parameters: + - in: path + name: offerId + schema: + type: integer + required: true + # description: Numeric ID of the user to get + +tags: + - name: Creation + description: Creating credentials + externalDocs: + url: https://docs.impierce.com/issuance diff --git a/agent_api_rest/src/issuance/credential_issuer/credential.rs b/agent_api_rest/src/issuance/credential_issuer/credential.rs index a1477122..3e9b2d35 100644 --- a/agent_api_rest/src/issuance/credential_issuer/credential.rs +++ b/agent_api_rest/src/issuance/credential_issuer/credential.rs @@ -27,15 +27,18 @@ use tracing::{error, info}; const DEFAULT_EXTERNAL_SERVER_RESPONSE_TIMEOUT_MS: u64 = 1000; const POLLING_INTERVAL_MS: u64 = 100; -/// Credential endpoint +/// Fetch credentials /// -/// Do something with credentials +/// A wallet uses this endpoint as part of an issuance flow to fetch the credentials from. #[utoipa::path( - get, - path = "/credential", - tag = "credential", + post, + path = "/authorization_requests", + // TODO: doesn't work since (external) `CredentialRequest` doesn't implement `ToSchema`? + // See: https://github.com/juhaku/utoipa?tab=readme-ov-file#how-to-implement-toschema-for-external-type + request_body = CredentialRequest, + tag = "openid4vci", responses( - (status = 200, description = "List all credentials successfully", body = [CredentialRequest]) + (status = 200, description = "List all credentials successfully") ) )] #[axum_macros::debug_handler] diff --git a/agent_api_rest/src/issuance/credential_issuer/mod.rs b/agent_api_rest/src/issuance/credential_issuer/mod.rs index 97c53c6d..df6bc6fe 100644 --- a/agent_api_rest/src/issuance/credential_issuer/mod.rs +++ b/agent_api_rest/src/issuance/credential_issuer/mod.rs @@ -1,10 +1,3 @@ pub mod credential; pub mod token; pub mod well_known; - -#[derive(utoipa::OpenApi)] -#[openapi( - paths(crate::issuance::credential_issuer::credential::credential), - // components(schemas(Todo, TodoError)) -)] -pub(crate) struct CredentialApi; diff --git a/agent_api_rest/src/issuance/credential_issuer/well_known/mod.rs b/agent_api_rest/src/issuance/credential_issuer/well_known/mod.rs index 65cac477..3e7935ad 100644 --- a/agent_api_rest/src/issuance/credential_issuer/well_known/mod.rs +++ b/agent_api_rest/src/issuance/credential_issuer/well_known/mod.rs @@ -1,9 +1,2 @@ pub mod oauth_authorization_server; pub mod openid_credential_issuer; - -#[derive(utoipa::OpenApi)] -#[openapi( - paths(crate::issuance::credential_issuer::well_known::oauth_authorization_server::oauth_authorization_server), - // components(schemas(Todo, TodoError)) -)] -pub(crate) struct WellKnownApi; 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 fd763ae0..3f35617f 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 @@ -9,15 +9,15 @@ use axum::{ response::{IntoResponse, Response}, }; -/// List all Todo items +/// Authorization Server Metadata /// -/// List all Todo items from in-memory storage. +/// Returns the Authorization Server Metadata. #[utoipa::path( get, path = "/oauth-authorization-server", - tag = "todo", + tag = "Well-Known", responses( - (status = 200, description = "List all todos successfully", body = [Todo]) + (status = 200, description = "Successfully returns the Authorization Server Metadata", body = [AuthorizationServerMetadata]) ) )] #[axum_macros::debug_handler] 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..f15ae957 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 @@ -9,6 +9,17 @@ use axum::{ response::{IntoResponse, Response}, }; +/// Credential Issuer Metadata +/// +/// Returns the Credential Issuer Metadata. +#[utoipa::path( + get, + path = "/openid-credential-issuer", + tag = "Well-Known", + responses( + (status = 200, description = "Successfully returns the Credential Issuer Metadata", body = [CredentialIssuerMetadata]) + ) +)] #[axum_macros::debug_handler] pub(crate) async fn openid_credential_issuer(State(state): State) -> Response { match query_handler(SERVER_CONFIG_ID, &state.query.server_config).await { diff --git a/agent_api_rest/src/issuance/credentials.rs b/agent_api_rest/src/issuance/credentials.rs index 2689f27c..626a5aad 100644 --- a/agent_api_rest/src/issuance/credentials.rs +++ b/agent_api_rest/src/issuance/credentials.rs @@ -16,7 +16,22 @@ use oid4vci::credential_issuer::credential_issuer_metadata::CredentialIssuerMeta use serde::{Deserialize, Serialize}; use serde_json::Value; use tracing::info; +use utoipa::ToSchema; +/// Retrieve a credential +/// +/// Return a credential for a given ID. +#[utoipa::path( + get, + path = "/credentials/{id}", + tag = "Credentials", + responses( + (status = 200, description = "Credential found", body = [CredentialView]) + ), + params( + ("id" = u64, Path, description = "Unique identitfier of the Credential"), + ) +)] #[axum_macros::debug_handler] pub(crate) async fn get_credentials(State(state): State, Path(credential_id): Path) -> Response { // Get the credential if it exists. @@ -30,7 +45,7 @@ pub(crate) async fn get_credentials(State(state): State, Path(cre } } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CredentialsEndpointRequest { pub offer_id: String, @@ -40,6 +55,18 @@ pub struct CredentialsEndpointRequest { pub credential_configuration_id: String, } +/// Create a new credential +/// +/// Create a new credential for the given payload. +#[utoipa::path( + post, + path = "/credentials", + request_body = CredentialsEndpointRequest, + tag = "Credentials", + responses( + (status = 200, description = "Successfully created a new credential.") + ) +)] #[axum_macros::debug_handler] pub(crate) async fn credentials( State(state): State, diff --git a/agent_api_rest/src/issuance/mod.rs b/agent_api_rest/src/issuance/mod.rs index 954f1c40..df64e6df 100644 --- a/agent_api_rest/src/issuance/mod.rs +++ b/agent_api_rest/src/issuance/mod.rs @@ -1,3 +1,4 @@ pub mod credential_issuer; pub mod credentials; pub mod offers; +pub mod openapi; diff --git a/agent_api_rest/src/issuance/offers.rs b/agent_api_rest/src/issuance/offers.rs index be3ea8d7..6efdb08b 100644 --- a/agent_api_rest/src/issuance/offers.rs +++ b/agent_api_rest/src/issuance/offers.rs @@ -13,13 +13,26 @@ use hyper::header; use serde::{Deserialize, Serialize}; use serde_json::Value; use tracing::info; +use utoipa::ToSchema; -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct OffersEndpointRequest { pub offer_id: String, } +/// Create a new offer +/// +/// Create a new credential offer for the given ID. +#[utoipa::path( + post, + path = "/offers", + request_body = OffersEndpointRequest, + tag = "Offers", + responses( + (status = 200, description = "Successfully created a new credential offer.") + ) +)] #[axum_macros::debug_handler] pub(crate) async fn offers(State(state): State, Json(payload): Json) -> Response { info!("Request Body: {}", payload); diff --git a/agent_api_rest/src/issuance/openapi.rs b/agent_api_rest/src/issuance/openapi.rs new file mode 100644 index 00000000..28fb43af --- /dev/null +++ b/agent_api_rest/src/issuance/openapi.rs @@ -0,0 +1,29 @@ +use utoipa::OpenApi; + +use crate::issuance::credential_issuer::credential; +use crate::issuance::credential_issuer::well_known::{oauth_authorization_server, openid_credential_issuer}; +use crate::issuance::credentials::{self, CredentialsEndpointRequest}; +use crate::issuance::offers; +use crate::verification::authorization_requests; + +#[derive(OpenApi)] +#[openapi( + // paths(credential::credential, credentials, get_credentials), + paths(credential::credential, credentials::credentials, credentials::get_credentials, offers::offers), + components(schemas(CredentialsEndpointRequest)) +)] +pub(crate) struct IssuanceApi; + +#[derive(OpenApi)] +#[openapi(paths( + authorization_requests::authorization_requests, + authorization_requests::get_authorization_requests +))] +pub(crate) struct VerificationApi; + +#[derive(OpenApi)] +#[openapi( + paths(oauth_authorization_server::oauth_authorization_server, openid_credential_issuer::openid_credential_issuer), + // components(schemas(Todo, TodoError)) +)] +pub(crate) struct WellKnownApi; diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index 49810b02..8577752d 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -25,11 +25,14 @@ use tower_http::trace::TraceLayer; use tracing::{info_span, Span}; use utoipa::OpenApi; use utoipa_scalar::{Scalar, Servable}; +use utoipa_swagger_ui::SwaggerUi; use verification::{ authorization_requests::{authorization_requests, get_authorization_requests}, relying_party::{redirect::redirect, request::request}, }; +use crate::issuance::openapi::{IssuanceApi, VerificationApi, WellKnownApi}; + pub const API_VERSION: &str = "/v0"; pub type ApplicationState = (IssuanceState, VerificationState); @@ -45,9 +48,9 @@ pub fn app(state: ApplicationState) -> Router { } }; - Router::new().merge(Scalar::with_url("/scalar", ApiDoc::openapi())); - Router::new() + .merge(Scalar::with_url("/scalar", ApiDoc::openapi())) + .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi())) .nest( &path(API_VERSION), Router::new() @@ -134,14 +137,17 @@ fn get_base_path() -> Result { #[openapi( // modifiers(), nest( - (path = "/.well-known", api = crate::issuance::credential_issuer::well_known::WellKnownApi), - (path = "/v0/credential", api = crate::issuance::credential_issuer::CredentialApi), + (path = "/.well-known", api = WellKnownApi), + (path = "/v0", api = IssuanceApi), + (path = "/v0", api = VerificationApi) ), // paths( // crate::issuance::credential_issuer::CredentialApi // ), tags( - (name = "todo", description = "Todo items management API") + // (name = "todo", description = "Todo items management API"), + (name = "openid4vci", description = "All operations revolved around the OpenID4VCI standard.", external_docs(url = "https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html", description = "OpenID for Verifiable Credential Issuance")), + (name = "Well-Known", description = "Well-known endpoints provide metadata about the server."), ) )] pub struct ApiDoc; @@ -209,6 +215,7 @@ mod tests { async fn openapi() { let yaml = serde_yaml::to_string(&ApiDoc::openapi()).unwrap(); println!("{}", yaml); + std::fs::write("test.openapi.yaml", yaml).unwrap(); } #[tokio::test] diff --git a/agent_api_rest/src/verification/authorization_requests.rs b/agent_api_rest/src/verification/authorization_requests.rs index ad79ff15..d588002d 100644 --- a/agent_api_rest/src/verification/authorization_requests.rs +++ b/agent_api_rest/src/verification/authorization_requests.rs @@ -18,7 +18,19 @@ use oid4vp::PresentationDefinition; use serde::{Deserialize, Serialize}; use serde_json::Value; use tracing::info; - +use utoipa::ToSchema; + +/// Get an authorization request +/// +/// Foobar +#[utoipa::path( + get, + path = "/authorization_requests/{id}", + tag = "Verification", + responses( + (status = 200, description = "") + ) +)] #[axum_macros::debug_handler] pub(crate) async fn get_authorization_requests( State(state): State, @@ -35,7 +47,7 @@ pub(crate) async fn get_authorization_requests( } } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, ToSchema)] pub struct AuthorizationRequestsEndpointRequest { pub nonce: String, pub state: Option, @@ -50,6 +62,18 @@ pub enum PresentationDefinitionResource { PresentationDefinition(PresentationDefinition), } +/// Create an authorization request +/// +/// Foobar +#[utoipa::path( + post, + path = "/authorization_requests", + request_body = AuthorizationRequestsEndpointRequest, + tag = "Verification", + responses( + (status = 200, description = "") + ) +)] #[axum_macros::debug_handler] pub(crate) async fn authorization_requests( State(verification_state): State, diff --git a/agent_api_rest/test.openapi.yaml b/agent_api_rest/test.openapi.yaml new file mode 100644 index 00000000..c12b7df8 --- /dev/null +++ b/agent_api_rest/test.openapi.yaml @@ -0,0 +1,151 @@ +openapi: 3.1.0 +info: + title: agent_api_rest + description: '' + license: + name: '' + version: 0.1.0 +paths: + /.well-known/oauth-authorization-server: + get: + tags: + - Well-Known + summary: Authorization Server Metadata + description: Returns the Authorization Server Metadata. + operationId: oauth_authorization_server + responses: + '200': + description: Successfully returns the Authorization Server Metadata + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/AuthorizationServerMetadata' + /.well-known/openid-credential-issuer: + get: + tags: + - Well-Known + summary: Credential Issuer Metadata + description: Returns the Credential Issuer Metadata. + operationId: openid_credential_issuer + responses: + '200': + description: Successfully returns the Credential Issuer Metadata + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CredentialIssuerMetadata' + /v0/authorization_requests: + post: + tags: + - Verification + summary: Create an authorization request + description: Foobar + operationId: authorization_requests + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AuthorizationRequestsEndpointRequest' + required: true + responses: + '200': + description: '' + /v0/authorization_requests/{id}: + get: + tags: + - Verification + summary: Get an authorization request + description: Foobar + operationId: get_authorization_requests + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + /v0/credentials: + post: + tags: + - Credentials + summary: Create a new credential + description: Create a new credential for the given payload. + operationId: credentials + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CredentialsEndpointRequest' + required: true + responses: + '200': + description: Successfully created a new credential. + /v0/credentials/{id}: + get: + tags: + - Credentials + summary: Retrieve a credential + description: Return a credential for a given ID. + operationId: get_credentials + parameters: + - name: id + in: path + description: Unique identitfier of the Credential + required: true + schema: + type: string + responses: + '200': + description: Credential found + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CredentialView' + /v0/offers: + post: + tags: + - Offers + summary: Create a new offer + description: Create a new credential offer for the given ID. + operationId: offers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OffersEndpointRequest' + required: true + responses: + '200': + description: Successfully created a new credential offer. +components: + schemas: + CredentialsEndpointRequest: + type: object + required: + - offerId + - credential + - credentialConfigurationId + properties: + credential: {} + credentialConfigurationId: + type: string + isSigned: + type: boolean + offerId: + type: string +tags: +- name: openid4vci + description: All operations revolved around the OpenID4VCI standard. + externalDocs: + url: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html + description: OpenID for Verifiable Credential Issuance +- name: Well-Known + description: Well-known endpoints provide metadata about the server. From ebfc36d6aa5d50bf30734e9e7f2fb95783686eb9 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Mon, 26 Aug 2024 08:22:43 +0200 Subject: [PATCH 04/48] 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 05/48] 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 06/48] 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 07/48] 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 08/48] 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 09/48] 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 10/48] 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 11/48] 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 12/48] 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 13/48] 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 14/48] 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 15/48] 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 16/48] 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 17/48] 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 18/48] 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 19/48] 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 20/48] 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 21/48] 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 22/48] 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 23/48] 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 24/48] 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 25/48] 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 26/48] 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 27/48] 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 28/48] 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 29/48] 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 30/48] 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 31/48] 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 4e8793f9751f1a9b7e9d7961759c50b08a53dd1a Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Fri, 30 Aug 2024 23:15:30 +0200 Subject: [PATCH 32/48] docs: add basic docs for holder API --- agent_api_rest/src/holder/holder/credentials/mod.rs | 11 +++++++++++ agent_api_rest/src/holder/holder/offers/accept.rs | 12 ++++++++++++ agent_api_rest/src/holder/holder/offers/mod.rs | 11 +++++++++++ agent_api_rest/src/holder/holder/offers/reject.rs | 12 ++++++++++++ agent_api_rest/src/lib.rs | 12 +++++++++--- agent_api_rest/src/{issuance => }/openapi.rs | 13 +++++++++++-- 6 files changed, 66 insertions(+), 5 deletions(-) rename agent_api_rest/src/{issuance => }/openapi.rs (73%) diff --git a/agent_api_rest/src/holder/holder/credentials/mod.rs b/agent_api_rest/src/holder/holder/credentials/mod.rs index 5e91880c..e9eab710 100644 --- a/agent_api_rest/src/holder/holder/credentials/mod.rs +++ b/agent_api_rest/src/holder/holder/credentials/mod.rs @@ -8,6 +8,17 @@ use axum::{ use hyper::StatusCode; use serde_json::json; +/// Get all credentials +/// +/// Retrieve all credentials that UniCore currently holds. +#[utoipa::path( + get, + path = "/holder/credentials", + tag = "Holder", + responses( + (status = 200, description = "Successfully retrieved all credentials."), + ) +)] #[axum_macros::debug_handler] pub(crate) async fn credentials(State(state): State) -> Response { match query_handler("all_credentials", &state.query.all_credentials).await { diff --git a/agent_api_rest/src/holder/holder/offers/accept.rs b/agent_api_rest/src/holder/holder/offers/accept.rs index 0cf1a0bb..47b2ece8 100644 --- a/agent_api_rest/src/holder/holder/offers/accept.rs +++ b/agent_api_rest/src/holder/holder/offers/accept.rs @@ -10,6 +10,18 @@ use axum::{ }; use hyper::StatusCode; +/// Accept an offer +/// +/// Accepts a pending credential offer. +#[utoipa::path( + post, + path = "/holder/offers/{offer_id}/accept", + // request_body = ?, + tag = "Holder", + responses( + (status = 200, description = "Successfully accepted a pending offer."), + ) +)] #[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 diff --git a/agent_api_rest/src/holder/holder/offers/mod.rs b/agent_api_rest/src/holder/holder/offers/mod.rs index c513aecd..728c6ce9 100644 --- a/agent_api_rest/src/holder/holder/offers/mod.rs +++ b/agent_api_rest/src/holder/holder/offers/mod.rs @@ -11,6 +11,17 @@ use axum::{ use hyper::StatusCode; use serde_json::json; +/// Get all offers +/// +/// Retrieve all pending credential offers. +#[utoipa::path( + get, + path = "/holder/offers", + tag = "Holder", + responses( + (status = 200, description = "Successfully retrieved all pending offers."), + ) +)] #[axum_macros::debug_handler] pub(crate) async fn offers(State(state): State) -> Response { match query_handler("all_offers", &state.query.all_offers).await { diff --git a/agent_api_rest/src/holder/holder/offers/reject.rs b/agent_api_rest/src/holder/holder/offers/reject.rs index eb0ffe17..c8d8311f 100644 --- a/agent_api_rest/src/holder/holder/offers/reject.rs +++ b/agent_api_rest/src/holder/holder/offers/reject.rs @@ -6,6 +6,18 @@ use axum::{ }; use hyper::StatusCode; +/// Reject an offer +/// +/// Reject a pending credential offer. +#[utoipa::path( + post, + path = "/holder/offers/{offer_id}/reject", + // request_body = ?, + tag = "Holder", + responses( + (status = 200, description = "Successfully rejected a pending offer."), + ) +)] #[axum_macros::debug_handler] pub(crate) async fn reject(State(state): State, Path(offer_id): Path) -> Response { let command = OfferCommand::RejectCredentialOffer { diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index 5067ea9a..e315df53 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -1,5 +1,6 @@ pub mod holder; pub mod issuance; +pub mod openapi; pub mod verification; use agent_holder::state::HolderState; @@ -17,7 +18,7 @@ use verification::{ relying_party::{redirect::redirect, request::request}, }; -use crate::issuance::openapi::{IssuanceApi, VerificationApi, WellKnownApi}; +use crate::openapi::{HolderApi, IssuanceApi, VerificationApi, WellKnownApi}; pub const API_VERSION: &str = "/v0"; @@ -104,14 +105,19 @@ fn get_base_path() -> Result { nest( (path = "/.well-known", api = WellKnownApi), (path = "/v0", api = IssuanceApi), - (path = "/v0", api = VerificationApi) + (path = "/v0", api = VerificationApi), + (path = "/v0", api = HolderApi) + ), + paths( + crate::holder::openid4vci::offers, + crate::issuance::credential_issuer::credential::credential, ), // paths( // crate::issuance::credential_issuer::CredentialApi // ), tags( // (name = "todo", description = "Todo items management API"), - (name = "openid4vci", description = "All operations revolved around the OpenID4VCI standard.", external_docs(url = "https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html", description = "OpenID for Verifiable Credential Issuance")), + (name = "OpenID4VCI", description = "All operations revolved around the OpenID4VCI standard.", external_docs(url = "https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html", description = "OpenID for Verifiable Credential Issuance")), (name = "Well-Known", description = "Well-known endpoints provide metadata about the server."), ) )] diff --git a/agent_api_rest/src/issuance/openapi.rs b/agent_api_rest/src/openapi.rs similarity index 73% rename from agent_api_rest/src/issuance/openapi.rs rename to agent_api_rest/src/openapi.rs index 28fb43af..44fc8dc7 100644 --- a/agent_api_rest/src/issuance/openapi.rs +++ b/agent_api_rest/src/openapi.rs @@ -1,6 +1,6 @@ use utoipa::OpenApi; -use crate::issuance::credential_issuer::credential; +use crate::holder::holder; use crate::issuance::credential_issuer::well_known::{oauth_authorization_server, openid_credential_issuer}; use crate::issuance::credentials::{self, CredentialsEndpointRequest}; use crate::issuance::offers; @@ -9,7 +9,7 @@ use crate::verification::authorization_requests; #[derive(OpenApi)] #[openapi( // paths(credential::credential, credentials, get_credentials), - paths(credential::credential, credentials::credentials, credentials::get_credentials, offers::offers), + paths(credentials::credentials, credentials::get_credentials, offers::offers), components(schemas(CredentialsEndpointRequest)) )] pub(crate) struct IssuanceApi; @@ -21,6 +21,15 @@ pub(crate) struct IssuanceApi; ))] pub(crate) struct VerificationApi; +#[derive(OpenApi)] +#[openapi(paths( + holder::credentials::credentials, + holder::offers::offers, + holder::offers::accept::accept, + holder::offers::reject::reject +))] +pub(crate) struct HolderApi; + #[derive(OpenApi)] #[openapi( paths(oauth_authorization_server::oauth_authorization_server, openid_credential_issuer::openid_credential_issuer), From c601e3fbe60b6a37f0bfe90270208869da34cfc1 Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Fri, 30 Aug 2024 23:15:55 +0200 Subject: [PATCH 33/48] chore: Update OpenID4VCI endpoint paths and tags --- agent_api_rest/src/holder/openid4vci/mod.rs | 11 +++ .../issuance/credential_issuer/credential.rs | 4 +- agent_api_rest/src/issuance/mod.rs | 1 - agent_api_rest/test.openapi.yaml | 86 ++++++++++++++++++- 4 files changed, 98 insertions(+), 4 deletions(-) diff --git a/agent_api_rest/src/holder/openid4vci/mod.rs b/agent_api_rest/src/holder/openid4vci/mod.rs index 95145b61..351c2a29 100644 --- a/agent_api_rest/src/holder/openid4vci/mod.rs +++ b/agent_api_rest/src/holder/openid4vci/mod.rs @@ -16,6 +16,17 @@ pub struct Oid4vciOfferEndpointRequest { pub credential_offer: CredentialOffer, } +/// Get an offer +/// +/// Retrieve offer if it exists. +#[utoipa::path( + get, + path = "/openid4vci/offers", + tag = "OpenID4VCI", + responses( + (status = 200, description = "Successfully retrieved offer."), + ) +)] #[axum_macros::debug_handler] pub(crate) async fn offers(State(state): State, Json(payload): Json) -> Response { info!("Request Body: {}", payload); diff --git a/agent_api_rest/src/issuance/credential_issuer/credential.rs b/agent_api_rest/src/issuance/credential_issuer/credential.rs index 51bf6e02..5bbdaee2 100644 --- a/agent_api_rest/src/issuance/credential_issuer/credential.rs +++ b/agent_api_rest/src/issuance/credential_issuer/credential.rs @@ -32,11 +32,11 @@ const POLLING_INTERVAL_MS: u64 = 100; /// A wallet uses this endpoint as part of an issuance flow to fetch the credentials from. #[utoipa::path( post, - path = "/authorization_requests", + path = "/openid4vci/credential", // TODO: doesn't work since (external) `CredentialRequest` doesn't implement `ToSchema`? // See: https://github.com/juhaku/utoipa?tab=readme-ov-file#how-to-implement-toschema-for-external-type request_body = CredentialRequest, - tag = "openid4vci", + tag = "OpenID4VCI", responses( (status = 200, description = "List all credentials successfully") ) diff --git a/agent_api_rest/src/issuance/mod.rs b/agent_api_rest/src/issuance/mod.rs index 76320065..0bf4064e 100644 --- a/agent_api_rest/src/issuance/mod.rs +++ b/agent_api_rest/src/issuance/mod.rs @@ -1,7 +1,6 @@ pub mod credential_issuer; pub mod credentials; pub mod offers; -pub mod openapi; use agent_issuance::state::IssuanceState; use axum::routing::get; diff --git a/agent_api_rest/test.openapi.yaml b/agent_api_rest/test.openapi.yaml index c12b7df8..41037513 100644 --- a/agent_api_rest/test.openapi.yaml +++ b/agent_api_rest/test.openapi.yaml @@ -38,6 +38,38 @@ paths: type: array items: $ref: '#/components/schemas/CredentialIssuerMetadata' + /openid4vci/credential: + post: + tags: + - OpenID4VCI + summary: Fetch credentials + description: A wallet uses this endpoint as part of an issuance flow to fetch the credentials from. + operationId: credential + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CredentialRequest' + required: true + responses: + '200': + description: List all credentials successfully + /openid4vci/offers: + get: + tags: + - OpenID4VCI + summary: Get an offer + description: Retrieve offer if it exists. + operationId: offers + requestBody: + description: '' + content: + application/json: + schema: {} + required: true + responses: + '200': + description: Successfully retrieved offer. /v0/authorization_requests: post: tags: @@ -109,6 +141,58 @@ paths: type: array items: $ref: '#/components/schemas/CredentialView' + /v0/holder/credentials: + get: + tags: + - Holder + summary: Get all credentials + description: Retrieve all credentials that UniCore currently holds. + operationId: credentials + responses: + '200': + description: Successfully retrieved all credentials. + /v0/holder/offers: + get: + tags: + - Holder + summary: Get all offers + description: Retrieve all pending credential offers. + operationId: offers + responses: + '200': + description: Successfully retrieved all pending offers. + /v0/holder/offers/{offer_id}/accept: + post: + tags: + - Holder + summary: Accept an offer + description: Accepts a pending credential offer. + operationId: accept + parameters: + - name: offer_id + in: path + required: true + schema: + type: string + responses: + '200': + description: Successfully accepted a pending offer. + /v0/holder/offers/{offer_id}/reject: + post: + tags: + - Holder + summary: Reject an offer + description: Reject a pending credential offer. + operationId: reject + parameters: + - name: offer_id + in: path + required: true + schema: + type: string + responses: + '200': + description: Successfully rejected a pending offer. /v0/offers: post: tags: @@ -142,7 +226,7 @@ components: offerId: type: string tags: -- name: openid4vci +- name: OpenID4VCI description: All operations revolved around the OpenID4VCI standard. externalDocs: url: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html From 304a469e043e8d06bf75d08e647664e75d85084b Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Sat, 31 Aug 2024 00:01:02 +0200 Subject: [PATCH 34/48] docs: manually patch the generated OpenAPI document --- agent_api_rest/src/lib.rs | 22 ++++++++++++++++------ agent_api_rest/test.openapi.yaml | 4 +++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index e315df53..9db679ba 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -10,7 +10,7 @@ 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 utoipa::OpenApi; +use utoipa::{openapi::ServerBuilder, OpenApi}; use utoipa_scalar::{Scalar, Servable}; use utoipa_swagger_ui::SwaggerUi; use verification::{ @@ -37,8 +37,8 @@ pub fn app( }: ApplicationState, ) -> Router { Router::new() - .merge(Scalar::with_url("/scalar", ApiDoc::openapi())) - .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi())) + .merge(Scalar::with_url("/scalar", patch_generated_openapi(ApiDoc::openapi()))) + .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", patch_generated_openapi(ApiDoc::openapi()))) .nest( &get_base_path().unwrap_or_default(), Router::new() @@ -123,6 +123,15 @@ fn get_base_path() -> Result { )] pub struct ApiDoc; +pub fn patch_generated_openapi(mut openapi: utoipa::openapi::OpenApi) -> utoipa::openapi::OpenApi { + openapi.info.title = "UniCore HTTP API".into(); + openapi.servers = vec![ServerBuilder::new() + .url("https://arty-aragorn.agent-dev.impierce.com") + .build()] + .into(); + openapi +} + #[cfg(test)] mod tests { use super::*; @@ -183,9 +192,10 @@ mod tests { #[tokio::test] async fn openapi() { - let yaml = serde_yaml::to_string(&ApiDoc::openapi()).unwrap(); - println!("{}", yaml); - std::fs::write("test.openapi.yaml", yaml).unwrap(); + let yaml_value = patch_generated_openapi(ApiDoc::openapi()); + let yaml_string = serde_yaml::to_string(&yaml_value).unwrap(); + println!("{}", yaml_string); + std::fs::write("test.openapi.yaml", yaml_string).unwrap(); } #[tokio::test] diff --git a/agent_api_rest/test.openapi.yaml b/agent_api_rest/test.openapi.yaml index 41037513..b3ed27cf 100644 --- a/agent_api_rest/test.openapi.yaml +++ b/agent_api_rest/test.openapi.yaml @@ -1,10 +1,12 @@ openapi: 3.1.0 info: - title: agent_api_rest + title: UniCore HTTP API description: '' license: name: '' version: 0.1.0 +servers: +- url: https://arty-aragorn.agent-dev.impierce.com paths: /.well-known/oauth-authorization-server: get: From dba22fe18e469539db228f1464bf3419f1fc0bbb Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Tue, 3 Sep 2024 11:11:39 +0200 Subject: [PATCH 35/48] docs: patch `description` and `version` --- agent_api_rest/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index 9db679ba..6e9270e6 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -125,6 +125,9 @@ pub struct ApiDoc; pub fn patch_generated_openapi(mut openapi: utoipa::openapi::OpenApi) -> utoipa::openapi::OpenApi { openapi.info.title = "UniCore HTTP API".into(); + openapi.info.description = Some("Full HTTP API reference for the UniCore SSI Agent".to_string()); + // openapi.info.version = "1.0.0-alpha.1".into(); // can this be determined or does it need to be removed from the openapi.yaml? + openapi.info.version = "".into(); openapi.servers = vec![ServerBuilder::new() .url("https://arty-aragorn.agent-dev.impierce.com") .build()] From 37750a528d4af74dd5abb9feefa9da2bcf99e14c Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Fri, 20 Sep 2024 18:09:09 +0200 Subject: [PATCH 36/48] refactor: remove unnecessary blank line --- agent_application/docker/db/init.sql | 1 - 1 file changed, 1 deletion(-) diff --git a/agent_application/docker/db/init.sql b/agent_application/docker/db/init.sql index f333905c..c0ce5c61 100644 --- a/agent_application/docker/db/init.sql +++ b/agent_application/docker/db/init.sql @@ -74,7 +74,6 @@ CREATE TABLE holder_credential PRIMARY KEY (view_id) ); - CREATE TABLE all_credentials ( view_id text NOT NULL, From 6ce460e58f28073b80a407fb29ea044a1d00da6b Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Fri, 20 Sep 2024 18:09:31 +0200 Subject: [PATCH 37/48] feat: remove file-based health check --- agent_application/src/main.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/agent_application/src/main.rs b/agent_application/src/main.rs index 23933869..20ed55bc 100644 --- a/agent_application/src/main.rs +++ b/agent_application/src/main.rs @@ -15,7 +15,7 @@ use agent_verification::services::VerificationServices; use axum::{routing::get, Json}; use identity_document::service::{Service, ServiceEndpoint}; use std::sync::Arc; -use tokio::{fs, io}; +use tokio::io; use tower_http::cors::CorsLayer; use tracing::info; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -155,12 +155,6 @@ async fn main() -> io::Result<()> { 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) - fs::create_dir_all("/tmp/unicore/").await?; - fs::write("/tmp/unicore/accept_requests", []).await?; - let listener = tokio::net::TcpListener::bind("0.0.0.0:3033").await?; info!("listening on {}", listener.local_addr()?); axum::serve(listener, app).await?; From 4cc16ad206778c9e1fba4a1d90cb455b789dc0bf Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Fri, 20 Sep 2024 18:10:08 +0200 Subject: [PATCH 38/48] chore(deps): bump `did_manager` --- Cargo.lock | 18 +++++++++--------- Cargo.toml | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0127c3df..e4e8f14a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1411,7 +1411,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", @@ -1977,7 +1977,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", @@ -1991,7 +1991,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", @@ -2008,7 +2008,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", @@ -2026,7 +2026,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", @@ -2054,7 +2054,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", @@ -3457,7 +3457,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", @@ -5434,7 +5434,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", @@ -6640,7 +6640,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", diff --git a/Cargo.toml b/Cargo.toml index c913ede1..b90dd68f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ 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" } +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 = "23facd4" } oid4vci = { git = "https://git@github.com/impierce/openid4vc.git", rev = "23facd4" } oid4vc-core = { git = "https://git@github.com/impierce/openid4vc.git", rev = "23facd4" } From aff2acdb3d0cd8cfd83d5cdd59fb2aef18c07867 Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Fri, 20 Sep 2024 18:14:18 +0200 Subject: [PATCH 39/48] refactor: rename generated file --- agent_api_rest/{test.openapi.yaml => generated.openapi.yaml} | 4 ++-- agent_api_rest/src/lib.rs | 2 +- agent_holder/src/offer/aggregate.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename agent_api_rest/{test.openapi.yaml => generated.openapi.yaml} (98%) diff --git a/agent_api_rest/test.openapi.yaml b/agent_api_rest/generated.openapi.yaml similarity index 98% rename from agent_api_rest/test.openapi.yaml rename to agent_api_rest/generated.openapi.yaml index b3ed27cf..7c9f4e3d 100644 --- a/agent_api_rest/test.openapi.yaml +++ b/agent_api_rest/generated.openapi.yaml @@ -1,10 +1,10 @@ openapi: 3.1.0 info: title: UniCore HTTP API - description: '' + description: Full HTTP API reference for the UniCore SSI Agent license: name: '' - version: 0.1.0 + version: '' servers: - url: https://arty-aragorn.agent-dev.impierce.com paths: diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index 6e9270e6..1ac45416 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -198,7 +198,7 @@ mod tests { let yaml_value = patch_generated_openapi(ApiDoc::openapi()); let yaml_string = serde_yaml::to_string(&yaml_value).unwrap(); println!("{}", yaml_string); - std::fs::write("test.openapi.yaml", yaml_string).unwrap(); + std::fs::write("generated.openapi.yaml", yaml_string).unwrap(); } #[tokio::test] diff --git a/agent_holder/src/offer/aggregate.rs b/agent_holder/src/offer/aggregate.rs index f3ab0f8f..32b27736 100644 --- a/agent_holder/src/offer/aggregate.rs +++ b/agent_holder/src/offer/aggregate.rs @@ -12,7 +12,7 @@ use oid4vci::token_response::TokenResponse; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; -use tracing::info; +use tracing::{info, warn}; #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] pub enum Status { From c94a377ca2a16e72b230bbcaca8bb13a41e88365 Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Fri, 20 Sep 2024 19:19:22 +0200 Subject: [PATCH 40/48] chore(deps): bump `utoipa` --- Cargo.lock | 89 +++++---------------------------------- agent_api_rest/Cargo.toml | 24 +++++------ 2 files changed, 20 insertions(+), 93 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e4e8f14a..be285608 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,7 +98,6 @@ dependencies = [ "url", "utoipa", "utoipa-scalar", - "utoipa-swagger-ui", "uuid", "wiremock", ] @@ -3156,7 +3155,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2 0.5.7", "tokio", "tower-service", "tracing", @@ -5766,7 +5765,6 @@ checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ "base64 0.22.1", "bytes", - "futures-channel", "futures-core", "futures-util", "http 1.1.0", @@ -6029,40 +6027,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "rust-embed" -version = "8.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" -dependencies = [ - "rust-embed-impl", - "rust-embed-utils", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "8.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" -dependencies = [ - "proc-macro2", - "quote", - "rust-embed-utils", - "syn 2.0.67", - "walkdir", -] - -[[package]] -name = "rust-embed-utils" -version = "8.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" -dependencies = [ - "sha2 0.10.8", - "walkdir", -] - [[package]] name = "rust-ini" version = "0.19.0" @@ -6245,15 +6209,6 @@ dependencies = [ "cipher", ] -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "scc" version = "2.1.1" @@ -8156,8 +8111,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utoipa" -version = "5.0.0-alpha.2" -source = "git+https://github.com/juhaku/utoipa.git?rev=f2a7143#f2a7143fd92767046abc9433daedc15938afe1a2" +version = "5.0.0-beta.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fac56d240b49c629b9083c932ac20a23d926937e67c21ba209f836e2983d4f" dependencies = [ "indexmap 2.2.6", "serde", @@ -8168,8 +8124,9 @@ dependencies = [ [[package]] name = "utoipa-gen" -version = "5.0.0-alpha.2" -source = "git+https://github.com/juhaku/utoipa.git?rev=f2a7143#f2a7143fd92767046abc9433daedc15938afe1a2" +version = "5.0.0-beta.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31d88270777931b8133b119c953062bd41665bb8507841f7d433f46d2765e9d4" dependencies = [ "proc-macro2", "quote", @@ -8179,8 +8136,9 @@ dependencies = [ [[package]] name = "utoipa-scalar" -version = "0.2.0-alpha.0" -source = "git+https://github.com/juhaku/utoipa.git?rev=f2a7143#f2a7143fd92767046abc9433daedc15938afe1a2" +version = "0.2.0-beta.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc86065a210b8540e46d15e0844765d1d14eec7fd6221c2b0de8f6edde990648" dependencies = [ "axum 0.7.5", "serde", @@ -8188,23 +8146,6 @@ dependencies = [ "utoipa", ] -[[package]] -name = "utoipa-swagger-ui" -version = "7.1.1-alpha.2" -source = "git+https://github.com/juhaku/utoipa.git?rev=f2a7143#f2a7143fd92767046abc9433daedc15938afe1a2" -dependencies = [ - "axum 0.7.5", - "mime_guess", - "regex", - "reqwest 0.12.5", - "rust-embed", - "serde", - "serde_json", - "url", - "utoipa", - "zip", -] - [[package]] name = "uuid" version = "1.8.0" @@ -8246,16 +8187,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "want" version = "0.3.1" diff --git a/agent_api_rest/Cargo.toml b/agent_api_rest/Cargo.toml index 0fd8db8a..399f238d 100644 --- a/agent_api_rest/Cargo.toml +++ b/agent_api_rest/Cargo.toml @@ -27,21 +27,17 @@ tracing.workspace = true tracing-subscriber.workspace = true url.workspace = true uuid.workspace = true -# utoipa = { version = "=5.0.0-alpha.1", features = ["axum_extras", "yaml"] } -# utoipa-scalar = { version = "=0.2.0-alpha.0", features = ["axum"] } +utoipa = { version = "=5.0.0-beta.0", features = ["axum_extras", "yaml"] } +utoipa-scalar = { version = "=0.2.0-beta.0", features = ["axum"] } # TODO: wait for new release that contains PR juhaku/utoipa#1002 (current version `=5.0.0-alpha.1`) -utoipa = { git = "https://github.com/juhaku/utoipa.git", rev = "f2a7143", features = [ - "axum_extras", - "yaml", -] } -# TODO: wait for new release that contains PR juhaku/utoipa#1002 (current version `=5.0.0-alpha.1`) -utoipa-scalar = { git = "https://github.com/juhaku/utoipa.git", rev = "f2a7143", features = [ - "axum", -] } -# TODO: wait for new release that contains PR juhaku/utoipa#1002 (current version `=5.0.0-alpha.1`) -utoipa-swagger-ui = { git = "https://github.com/juhaku/utoipa.git", rev = "f2a7143", features = [ - "axum", -] } +# utoipa = { git = "https://github.com/juhaku/utoipa.git", rev = "f2a7143", features = [ +# "axum_extras", +# "yaml", +# ] } +# # TODO: wait for new release that contains PR juhaku/utoipa#1002 (current version `=5.0.0-alpha.1`) +# utoipa-scalar = { git = "https://github.com/juhaku/utoipa.git", rev = "f2a7143", features = [ +# "axum", +# ] } [dev-dependencies] agent_event_publisher_http = { path = "../agent_event_publisher_http", features = ["test_utils"] } From 33d52049524c1f6b5ca598a851a8024d79d0d735 Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Fri, 20 Sep 2024 19:19:48 +0200 Subject: [PATCH 41/48] docs: describe OpenAPI usage --- agent_api_rest/README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/agent_api_rest/README.md b/agent_api_rest/README.md index 4e55c2bf..d70cdadc 100644 --- a/agent_api_rest/README.md +++ b/agent_api_rest/README.md @@ -7,13 +7,20 @@ Breaking changes may occur before the API reaches a stable version. The current version of the REST API is `v0`. -### OpenAPI specification (Swagger UI) +### OpenAPI specification + +> [!NOTE] +> UniCore uses [Scalar](https://scalar.com) to make its OpenAPI specification interactive. It is served under `///api-reference` (for example: `/v0/api-reference`). The `openapi.yaml` file can be downloaded there as well. The latest version of the `openapi.yaml` file is also deployed as part of the documentation at https://docs.impierce.com/unicore/api-reference. + +#### Swagger UI + +You can also run a local Swagger UI container to inspect the OpenAPI specification. ```bash docker run --rm -p 9090:8080 -e SWAGGER_JSON=/tmp/openapi.yaml -v $(pwd):/tmp swaggerapi/swagger-ui ``` -Browse to http://localhost:9090 +Browse to http://localhost:9090. ### CORS From 2967c3a516b081adf8b852b501ec5cf992b833b5 Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Fri, 20 Sep 2024 19:20:04 +0200 Subject: [PATCH 42/48] refactor: remove Swagger UI --- agent_api_rest/src/lib.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index 1ac45416..25834feb 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -12,11 +12,6 @@ use tower_http::trace::TraceLayer; use tracing::{info_span, Span}; use utoipa::{openapi::ServerBuilder, OpenApi}; use utoipa_scalar::{Scalar, Servable}; -use utoipa_swagger_ui::SwaggerUi; -use verification::{ - authorization_requests::{authorization_requests, get_authorization_requests}, - relying_party::{redirect::redirect, request::request}, -}; use crate::openapi::{HolderApi, IssuanceApi, VerificationApi, WellKnownApi}; @@ -37,14 +32,17 @@ pub fn app( }: ApplicationState, ) -> Router { Router::new() - .merge(Scalar::with_url("/scalar", patch_generated_openapi(ApiDoc::openapi()))) - .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", patch_generated_openapi(ApiDoc::openapi()))) .nest( &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()) - .merge(verification_state.map(verification::router).unwrap_or_default()), + .merge(verification_state.map(verification::router).unwrap_or_default()) + // API Docs + .merge(Scalar::with_url( + format!("{}/api-reference", API_VERSION), + patch_generated_openapi(ApiDoc::openapi()), + )), ) // Trace layer .layer( @@ -194,7 +192,7 @@ mod tests { async fn handler() {} #[tokio::test] - async fn openapi() { + async fn generate_openapi_file() { let yaml_value = patch_generated_openapi(ApiDoc::openapi()); let yaml_string = serde_yaml::to_string(&yaml_value).unwrap(); println!("{}", yaml_string); From dd451c6f0338329ce5ea995d635a90f48229fb25 Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Fri, 20 Sep 2024 19:25:44 +0200 Subject: [PATCH 43/48] refactor: rename current `openapi.yaml` file (non-generated) --- agent_api_rest/{openapi.yaml => bak.openapi.yaml} | 0 agent_api_rest/src/lib.rs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename agent_api_rest/{openapi.yaml => bak.openapi.yaml} (100%) diff --git a/agent_api_rest/openapi.yaml b/agent_api_rest/bak.openapi.yaml similarity index 100% rename from agent_api_rest/openapi.yaml rename to agent_api_rest/bak.openapi.yaml diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index 25834feb..69c69cab 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -196,7 +196,7 @@ mod tests { let yaml_value = patch_generated_openapi(ApiDoc::openapi()); let yaml_string = serde_yaml::to_string(&yaml_value).unwrap(); println!("{}", yaml_string); - std::fs::write("generated.openapi.yaml", yaml_string).unwrap(); + std::fs::write("openapi.yaml", yaml_string).unwrap(); } #[tokio::test] From ca2ceaf0108f5f5e63ad3313e261ead84d4ce151 Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Sat, 21 Sep 2024 18:20:00 +0200 Subject: [PATCH 44/48] docs: update OpenAPI reference --- .../{generated.openapi.yaml => openapi.yaml} | 119 ++++++++++++++---- agent_api_rest/src/holder/openid4vci/mod.rs | 9 +- .../issuance/credential_issuer/credential.rs | 9 +- .../src/issuance/credential_issuer/token.rs | 11 ++ .../well_known/oauth_authorization_server.rs | 7 +- .../well_known/openid_credential_issuer.rs | 7 +- agent_api_rest/src/lib.rs | 35 +++--- agent_api_rest/src/openapi.rs | 8 -- .../verification/authorization_requests.rs | 12 +- .../verification/relying_party/redirect.rs | 11 ++ .../src/verification/relying_party/request.rs | 12 ++ 11 files changed, 169 insertions(+), 71 deletions(-) rename agent_api_rest/{generated.openapi.yaml => openapi.yaml} (62%) diff --git a/agent_api_rest/generated.openapi.yaml b/agent_api_rest/openapi.yaml similarity index 62% rename from agent_api_rest/generated.openapi.yaml rename to agent_api_rest/openapi.yaml index 7c9f4e3d..e2c1a80e 100644 --- a/agent_api_rest/generated.openapi.yaml +++ b/agent_api_rest/openapi.yaml @@ -7,13 +7,15 @@ info: version: '' servers: - url: https://arty-aragorn.agent-dev.impierce.com + description: UniCore development server hosted by Impierce Technologies paths: /.well-known/oauth-authorization-server: get: tags: - - Well-Known + - Well-known + - (public) summary: Authorization Server Metadata - description: Returns the Authorization Server Metadata. + description: Standard OpenID Connect discovery endpoint for authorization metadata. operationId: oauth_authorization_server responses: '200': @@ -27,9 +29,10 @@ paths: /.well-known/openid-credential-issuer: get: tags: - - Well-Known + - Well-known + - (public) summary: Credential Issuer Metadata - description: Returns the Credential Issuer Metadata. + description: Standard OpenID Connect discovery endpoint for issuer metadata. operationId: openid_credential_issuer responses: '200': @@ -40,12 +43,30 @@ paths: type: array items: $ref: '#/components/schemas/CredentialIssuerMetadata' + /auth/token: + post: + tags: + - (public) + summary: Token Endpoint + description: Standard OAuth 2.0 endpoint that returns an access_token after successful authorization. + operationId: token + requestBody: + description: '' + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/TokenRequest' + required: true + responses: + '200': + description: Returns an access token. /openid4vci/credential: post: tags: - - OpenID4VCI - summary: Fetch credentials - description: A wallet uses this endpoint as part of an issuance flow to fetch the credentials from. + - Issuance + - (public) + summary: Credential Endpoint + description: Standard OpenID4VCI endpoint for redeeming a token for a credential. operationId: credential requestBody: content: @@ -55,13 +76,20 @@ paths: required: true responses: '200': - description: List all credentials successfully + description: Successfully returns the credential + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CredentialResponse' /openid4vci/offers: get: tags: - - OpenID4VCI - summary: Get an offer - description: Retrieve offer if it exists. + - Holder + - (public) + summary: Credential Offer Endpoint + description: Standard OpenID4VCI endpoint that allows the issuer to pass information to the wallet. operationId: offers requestBody: description: '' @@ -71,13 +99,47 @@ paths: required: true responses: '200': - description: Successfully retrieved offer. + description: Successfully received offer metadata. + /redirect: + post: + tags: + - (public) + summary: Redirect Endpoint + description: Standard OAuth 2.0 endpoint. + operationId: redirect + requestBody: + description: '' + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/GenericAuthorizationResponse' + required: true + responses: {} + /request/{id}: + get: + tags: + - (public) + summary: Authorization Request + description: |- + Standard OAuth 2.0 endpoint. + + Instead of directly embedding the Authorization Request into a QR-code or deeplink, the `Relying Party` can embed a + `request_uri` that points to this endpoint from where the Authorization Request Object can be retrieved. + As described here: https://www.rfc-editor.org/rfc/rfc9101.html#name-passing-a-request-object-by- + operationId: request + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: {} /v0/authorization_requests: post: tags: - Verification - summary: Create an authorization request - description: Foobar + summary: Create a new Authorization Request + description: UniCore will ask a holder for certain information based on the Presentation Definition specified. operationId: authorization_requests requestBody: content: @@ -86,14 +148,14 @@ paths: $ref: '#/components/schemas/AuthorizationRequestsEndpointRequest' required: true responses: - '200': - description: '' + '201': + description: Authorization Request successfully created. /v0/authorization_requests/{id}: get: tags: - Verification - summary: Get an authorization request - description: Foobar + summary: Get an Authorization Request + description: Retrieve an existing Authorization Request. operationId: get_authorization_requests parameters: - name: id @@ -103,7 +165,13 @@ paths: type: string responses: '200': - description: '' + description: Successfully returns an existing Authorization Request. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/GenericAuthorizationRequest' /v0/credentials: post: tags: @@ -133,7 +201,9 @@ paths: description: Unique identitfier of the Credential required: true schema: - type: string + type: integer + format: int64 + minimum: 0 responses: '200': description: Credential found @@ -228,10 +298,7 @@ components: offerId: type: string tags: -- name: OpenID4VCI - description: All operations revolved around the OpenID4VCI standard. - externalDocs: - url: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html - description: OpenID for Verifiable Credential Issuance -- name: Well-Known +- name: (public) + description: A collection of endpoints that should be publicly accessible without authentication. They are used to resolve metadata or allow communication with wallets. +- name: Well-known description: Well-known endpoints provide metadata about the server. diff --git a/agent_api_rest/src/holder/openid4vci/mod.rs b/agent_api_rest/src/holder/openid4vci/mod.rs index 351c2a29..ccb4a5c2 100644 --- a/agent_api_rest/src/holder/openid4vci/mod.rs +++ b/agent_api_rest/src/holder/openid4vci/mod.rs @@ -16,15 +16,16 @@ pub struct Oid4vciOfferEndpointRequest { pub credential_offer: CredentialOffer, } -/// Get an offer +/// Credential Offer Endpoint /// -/// Retrieve offer if it exists. +/// Standard OpenID4VCI endpoint that allows the issuer to pass information to the wallet. #[utoipa::path( get, path = "/openid4vci/offers", - tag = "OpenID4VCI", + tag = "Holder", + tags = ["(public)"], responses( - (status = 200, description = "Successfully retrieved offer."), + (status = 200, description = "Successfully received offer metadata."), ) )] #[axum_macros::debug_handler] diff --git a/agent_api_rest/src/issuance/credential_issuer/credential.rs b/agent_api_rest/src/issuance/credential_issuer/credential.rs index 5bbdaee2..157526a8 100644 --- a/agent_api_rest/src/issuance/credential_issuer/credential.rs +++ b/agent_api_rest/src/issuance/credential_issuer/credential.rs @@ -27,18 +27,19 @@ use tracing::{error, info}; const DEFAULT_EXTERNAL_SERVER_RESPONSE_TIMEOUT_MS: u64 = 1000; const POLLING_INTERVAL_MS: u64 = 100; -/// Fetch credentials +/// Credential Endpoint /// -/// A wallet uses this endpoint as part of an issuance flow to fetch the credentials from. +/// Standard OpenID4VCI endpoint for redeeming a token for a credential. #[utoipa::path( post, path = "/openid4vci/credential", // TODO: doesn't work since (external) `CredentialRequest` doesn't implement `ToSchema`? // See: https://github.com/juhaku/utoipa?tab=readme-ov-file#how-to-implement-toschema-for-external-type request_body = CredentialRequest, - tag = "OpenID4VCI", + tag = "Issuance", + tags = ["(public)"], responses( - (status = 200, description = "List all credentials successfully") + (status = 200, description = "Successfully returns the credential", body = [CredentialResponse]) ) )] #[axum_macros::debug_handler] diff --git a/agent_api_rest/src/issuance/credential_issuer/token.rs b/agent_api_rest/src/issuance/credential_issuer/token.rs index 72728ad5..08ebfac7 100644 --- a/agent_api_rest/src/issuance/credential_issuer/token.rs +++ b/agent_api_rest/src/issuance/credential_issuer/token.rs @@ -16,6 +16,17 @@ use oid4vci::token_request::TokenRequest; use serde_json::json; use tracing::info; +/// Token Endpoint +/// +/// Standard OAuth 2.0 endpoint that returns an access_token after successful authorization. +#[utoipa::path( + post, + path = "/auth/token", + tags = ["(public)"], + responses( + (status = 200, description = "Returns an access token."), + ) +)] #[axum_macros::debug_handler] pub(crate) async fn token( State(state): 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 6b9784a0..b79aef77 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 @@ -11,11 +11,12 @@ use axum::{ /// Authorization Server Metadata /// -/// Returns the Authorization Server Metadata. +/// Standard OpenID Connect discovery endpoint for authorization metadata. #[utoipa::path( get, - path = "/oauth-authorization-server", - tag = "Well-Known", + path = "/.well-known/oauth-authorization-server", + tag = "Well-known", + tags = ["(public)"], responses( (status = 200, description = "Successfully returns the Authorization Server Metadata", body = [AuthorizationServerMetadata]) ) 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 2e96b433..fc4bdf4a 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 @@ -11,11 +11,12 @@ use axum::{ /// Credential Issuer Metadata /// -/// Returns the Credential Issuer Metadata. +/// Standard OpenID Connect discovery endpoint for issuer metadata. #[utoipa::path( get, - path = "/openid-credential-issuer", - tag = "Well-Known", + path = "/.well-known/openid-credential-issuer", + tag = "Well-known", + tags = ["(public)"], responses( (status = 200, description = "Successfully returns the Credential Issuer Metadata", body = [CredentialIssuerMetadata]) ) diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index 69c69cab..9957b0c3 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -13,7 +13,7 @@ use tracing::{info_span, Span}; use utoipa::{openapi::ServerBuilder, OpenApi}; use utoipa_scalar::{Scalar, Servable}; -use crate::openapi::{HolderApi, IssuanceApi, VerificationApi, WellKnownApi}; +use crate::openapi::{HolderApi, IssuanceApi, VerificationApi}; pub const API_VERSION: &str = "/v0"; @@ -93,30 +93,30 @@ fn get_base_path() -> Result { }) } -// #[derive(OpenApi)] -// #[openapi(modifiers(), nest((path = "/v0/todos", api = WellKnownApi)), tags((name = "well-known")))] -// struct ApiDoc; - #[derive(utoipa::OpenApi)] #[openapi( // modifiers(), + paths( + // Standard endpoints as defined in the protocol specifications. + // OAuth 2.0 + crate::verification::relying_party::redirect::redirect, + crate::verification::relying_party::request::request, + crate::issuance::credential_issuer::token::token, + // OpenID4VCI + crate::holder::openid4vci::offers, + crate::issuance::credential_issuer::credential::credential, + // .well-known + crate::issuance::credential_issuer::well_known::oauth_authorization_server::oauth_authorization_server, + crate::issuance::credential_issuer::well_known::openid_credential_issuer::openid_credential_issuer, + ), nest( - (path = "/.well-known", api = WellKnownApi), (path = "/v0", api = IssuanceApi), (path = "/v0", api = VerificationApi), (path = "/v0", api = HolderApi) ), - paths( - crate::holder::openid4vci::offers, - crate::issuance::credential_issuer::credential::credential, - ), - // paths( - // crate::issuance::credential_issuer::CredentialApi - // ), tags( - // (name = "todo", description = "Todo items management API"), - (name = "OpenID4VCI", description = "All operations revolved around the OpenID4VCI standard.", external_docs(url = "https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html", description = "OpenID for Verifiable Credential Issuance")), - (name = "Well-Known", description = "Well-known endpoints provide metadata about the server."), + (name = "(public)", description = "A collection of endpoints that should be publicly accessible without authentication. They are used to resolve metadata or allow communication with wallets."), + (name = "Well-known", description = "Well-known endpoints provide metadata about the server.") ) )] pub struct ApiDoc; @@ -124,10 +124,11 @@ pub struct ApiDoc; pub fn patch_generated_openapi(mut openapi: utoipa::openapi::OpenApi) -> utoipa::openapi::OpenApi { openapi.info.title = "UniCore HTTP API".into(); openapi.info.description = Some("Full HTTP API reference for the UniCore SSI Agent".to_string()); - // openapi.info.version = "1.0.0-alpha.1".into(); // can this be determined or does it need to be removed from the openapi.yaml? + // openapi.info.version = "1.0.0-alpha.1".into(); // can UniCore even be aware of its current version or does it need to be removed from the openapi.yaml? openapi.info.version = "".into(); openapi.servers = vec![ServerBuilder::new() .url("https://arty-aragorn.agent-dev.impierce.com") + .description(Some("UniCore development server hosted by Impierce Technologies")) .build()] .into(); openapi diff --git a/agent_api_rest/src/openapi.rs b/agent_api_rest/src/openapi.rs index 44fc8dc7..a2b2a6ce 100644 --- a/agent_api_rest/src/openapi.rs +++ b/agent_api_rest/src/openapi.rs @@ -1,7 +1,6 @@ use utoipa::OpenApi; use crate::holder::holder; -use crate::issuance::credential_issuer::well_known::{oauth_authorization_server, openid_credential_issuer}; use crate::issuance::credentials::{self, CredentialsEndpointRequest}; use crate::issuance::offers; use crate::verification::authorization_requests; @@ -29,10 +28,3 @@ pub(crate) struct VerificationApi; holder::offers::reject::reject ))] pub(crate) struct HolderApi; - -#[derive(OpenApi)] -#[openapi( - paths(oauth_authorization_server::oauth_authorization_server, openid_credential_issuer::openid_credential_issuer), - // components(schemas(Todo, TodoError)) -)] -pub(crate) struct WellKnownApi; diff --git a/agent_api_rest/src/verification/authorization_requests.rs b/agent_api_rest/src/verification/authorization_requests.rs index 9976dbdf..36c1cb49 100644 --- a/agent_api_rest/src/verification/authorization_requests.rs +++ b/agent_api_rest/src/verification/authorization_requests.rs @@ -20,15 +20,15 @@ use serde_json::Value; use tracing::info; use utoipa::ToSchema; -/// Get an authorization request +/// Get an Authorization Request /// -/// Foobar +/// Retrieve an existing Authorization Request. #[utoipa::path( get, path = "/authorization_requests/{id}", tag = "Verification", responses( - (status = 200, description = "") + (status = 200, description = "Successfully returns an existing Authorization Request.", body = [GenericAuthorizationRequest]) ) )] #[axum_macros::debug_handler] @@ -62,16 +62,16 @@ pub enum PresentationDefinitionResource { PresentationDefinition(PresentationDefinition), } -/// Create an authorization request +/// Create a new Authorization Request /// -/// Foobar +/// UniCore will ask a holder for certain information based on the Presentation Definition specified. #[utoipa::path( post, path = "/authorization_requests", request_body = AuthorizationRequestsEndpointRequest, tag = "Verification", responses( - (status = 200, description = "") + (status = 201, description = "Authorization Request successfully created."), ) )] #[axum_macros::debug_handler] diff --git a/agent_api_rest/src/verification/relying_party/redirect.rs b/agent_api_rest/src/verification/relying_party/redirect.rs index 7e315071..8e6258dd 100644 --- a/agent_api_rest/src/verification/relying_party/redirect.rs +++ b/agent_api_rest/src/verification/relying_party/redirect.rs @@ -10,6 +10,17 @@ use axum::{ Form, }; +/// Redirect Endpoint +/// +/// Standard OAuth 2.0 endpoint. +#[utoipa::path( + post, + path = "/redirect", + tags = ["(public)"], + responses( + // (status = 200, description = ""), + ) +)] #[axum_macros::debug_handler] pub(crate) async fn redirect( State(verification_state): State, diff --git a/agent_api_rest/src/verification/relying_party/request.rs b/agent_api_rest/src/verification/relying_party/request.rs index ff73e918..8c892791 100644 --- a/agent_api_rest/src/verification/relying_party/request.rs +++ b/agent_api_rest/src/verification/relying_party/request.rs @@ -7,9 +7,21 @@ use axum::{ }; use hyper::header; +/// Authorization Request +/// +/// Standard OAuth 2.0 endpoint. +/// /// Instead of directly embedding the Authorization Request into a QR-code or deeplink, the `Relying Party` can embed a /// `request_uri` that points to this endpoint from where the Authorization Request Object can be retrieved. /// As described here: https://www.rfc-editor.org/rfc/rfc9101.html#name-passing-a-request-object-by- +#[utoipa::path( + get, + path = "/request/{id}", + tags = ["(public)"], + responses( + // (status = 200, description = ""), + ) +)] #[axum_macros::debug_handler] pub(crate) async fn request( State(verification_state): State, From 5d20f17c4aac0c114fa24fe6fa9bb0e49ee7fd2e Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Sat, 21 Sep 2024 18:40:28 +0200 Subject: [PATCH 45/48] refactor: rename `Well-known` tag --- agent_api_rest/openapi.yaml | 13 +++++++++---- agent_api_rest/src/holder/holder/credentials/mod.rs | 2 +- .../well_known/oauth_authorization_server.rs | 2 +- .../well_known/openid_credential_issuer.rs | 2 +- agent_api_rest/src/lib.rs | 3 ++- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/agent_api_rest/openapi.yaml b/agent_api_rest/openapi.yaml index e2c1a80e..28c00b64 100644 --- a/agent_api_rest/openapi.yaml +++ b/agent_api_rest/openapi.yaml @@ -12,7 +12,7 @@ paths: /.well-known/oauth-authorization-server: get: tags: - - Well-known + - (.well-known) - (public) summary: Authorization Server Metadata description: Standard OpenID Connect discovery endpoint for authorization metadata. @@ -29,7 +29,7 @@ paths: /.well-known/openid-credential-issuer: get: tags: - - Well-known + - (.well-known) - (public) summary: Credential Issuer Metadata description: Standard OpenID Connect discovery endpoint for issuer metadata. @@ -218,7 +218,7 @@ paths: tags: - Holder summary: Get all credentials - description: Retrieve all credentials that UniCore currently holds. + description: Retrieve all credentials that this UniCore instance currently holds. operationId: credentials responses: '200': @@ -300,5 +300,10 @@ components: tags: - name: (public) description: A collection of endpoints that should be publicly accessible without authentication. They are used to resolve metadata or allow communication with wallets. -- name: Well-known +- name: (.well-known) description: Well-known endpoints provide metadata about the server. +- name: Issuance + description: Issue credentials to holders that will store them in their wallets. + externalDocs: + url: https://docs.impierce.com + description: Official Documentation diff --git a/agent_api_rest/src/holder/holder/credentials/mod.rs b/agent_api_rest/src/holder/holder/credentials/mod.rs index a4fd9fc8..4398df18 100644 --- a/agent_api_rest/src/holder/holder/credentials/mod.rs +++ b/agent_api_rest/src/holder/holder/credentials/mod.rs @@ -10,7 +10,7 @@ use serde_json::json; /// Get all credentials /// -/// Retrieve all credentials that UniCore currently holds. +/// Retrieve all credentials that this UniCore instance currently holds. #[utoipa::path( get, path = "/holder/credentials", 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 b79aef77..d99ba393 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 @@ -15,7 +15,7 @@ use axum::{ #[utoipa::path( get, path = "/.well-known/oauth-authorization-server", - tag = "Well-known", + tag = "(.well-known)", tags = ["(public)"], responses( (status = 200, description = "Successfully returns the Authorization Server Metadata", body = [AuthorizationServerMetadata]) 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 fc4bdf4a..c95bcd9b 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 @@ -15,7 +15,7 @@ use axum::{ #[utoipa::path( get, path = "/.well-known/openid-credential-issuer", - tag = "Well-known", + tag = "(.well-known)", tags = ["(public)"], responses( (status = 200, description = "Successfully returns the Credential Issuer Metadata", body = [CredentialIssuerMetadata]) diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index 9957b0c3..abcbae9c 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -116,7 +116,8 @@ fn get_base_path() -> Result { ), tags( (name = "(public)", description = "A collection of endpoints that should be publicly accessible without authentication. They are used to resolve metadata or allow communication with wallets."), - (name = "Well-known", description = "Well-known endpoints provide metadata about the server.") + (name = "(.well-known)", description = "Well-known endpoints provide metadata about the server."), + (name = "Issuance", description = "Issue credentials to holders that will store them in their wallets.", external_docs(description="Official Documentation", url="https://docs.impierce.com")), ) )] pub struct ApiDoc; From d99bcd2cb3466c4857d095c6db141c43d55850dc Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Sun, 29 Sep 2024 22:53:44 +0200 Subject: [PATCH 46/48] docs: add examples --- agent_api_rest/openapi.yaml | 78 +++++++++++++++---- agent_api_rest/src/issuance/credentials.rs | 28 +++++-- agent_api_rest/src/issuance/offers/mod.rs | 8 +- agent_api_rest/src/lib.rs | 8 +- .../verification/authorization_requests.rs | 3 + .../verification/relying_party/redirect.rs | 2 +- .../src/verification/relying_party/request.rs | 2 +- 7 files changed, 97 insertions(+), 32 deletions(-) diff --git a/agent_api_rest/openapi.yaml b/agent_api_rest/openapi.yaml index 28c00b64..e779a14d 100644 --- a/agent_api_rest/openapi.yaml +++ b/agent_api_rest/openapi.yaml @@ -6,7 +6,7 @@ info: name: '' version: '' servers: -- url: https://arty-aragorn.agent-dev.impierce.com +- url: https://playground.agent-dev.impierce.com description: UniCore development server hosted by Impierce Technologies paths: /.well-known/oauth-authorization-server: @@ -114,7 +114,9 @@ paths: schema: $ref: '#/components/schemas/GenericAuthorizationResponse' required: true - responses: {} + responses: + '200': + description: '' /request/{id}: get: tags: @@ -133,7 +135,9 @@ paths: required: true schema: type: string - responses: {} + responses: + '200': + description: '' /v0/authorization_requests: post: tags: @@ -160,6 +164,7 @@ paths: parameters: - name: id in: path + description: The ID of the Authorization Request to retrieve. required: true schema: type: string @@ -175,19 +180,56 @@ paths: /v0/credentials: post: tags: - - Credentials + - Issuance summary: Create a new credential - description: Create a new credential for the given payload. + description: Create a new credential for a given subject. operationId: credentials requestBody: content: application/json: schema: $ref: '#/components/schemas/CredentialsEndpointRequest' + examples: + openbadges: + summary: Open Badges 3.0 + description: s0me descr1pti0n + externalValue: res/open-badge-request.json + w3c-vc: + summary: W3C v1.1 + description: s0me descr1pti0n + value: + offerId: 123 + credentialConfigurationId: w3c_vc_credential + credential: + credentialSubject: + first_name: Ferris + last_name: Rustacean required: true responses: - '200': + '201': description: Successfully created a new credential. + headers: + Location: + schema: + type: string + description: URL of the created resource + content: + application/json: + schema: + $ref: '#/components/schemas/CredentialView' + examples: + openbadges-3-0: + summary: Open Badges 3.0 + description: An badge following the Open Badges Specification 3.0 + value: + foo: bar + w3c-vc-1-1: + summary: W3C VC Data Model v1.1 + description: A credential following the W3C Verifiable Credentials Data Model v1.1 + value: + offerId: '0001' + '400': + description: Invalid payload. /v0/credentials/{id}: get: tags: @@ -198,12 +240,10 @@ paths: parameters: - name: id in: path - description: Unique identitfier of the Credential + description: Unique identifier of the Credential required: true schema: - type: integer - format: int64 - minimum: 0 + type: string responses: '200': description: Credential found @@ -268,9 +308,9 @@ paths: /v0/offers: post: tags: - - Offers - summary: Create a new offer - description: Create a new credential offer for the given ID. + - Issuance + summary: Create a new credential offer + description: Create a new offer for one or more credentials. operationId: offers requestBody: content: @@ -280,7 +320,12 @@ paths: required: true responses: '200': - description: Successfully created a new credential offer. + description: Successfully created a new credential offer. Response value is standard-compliant and can be used by identity wallet. + content: + application/x-www-form-urlencoded: + schema: + type: string + example: openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fcredential-issuer.example.com%2F%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22type%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%7D%5D%7D components: schemas: CredentialsEndpointRequest: @@ -306,4 +351,7 @@ tags: description: Issue credentials to holders that will store them in their wallets. externalDocs: url: https://docs.impierce.com - description: Official Documentation + description: Issuance API Documentation +externalDocs: + url: https://docs.impierce.com + description: Official Documentation diff --git a/agent_api_rest/src/issuance/credentials.rs b/agent_api_rest/src/issuance/credentials.rs index 416c37b2..f66e2342 100644 --- a/agent_api_rest/src/issuance/credentials.rs +++ b/agent_api_rest/src/issuance/credentials.rs @@ -25,11 +25,11 @@ use utoipa::ToSchema; get, path = "/credentials/{id}", tag = "Credentials", + params( + ("id" = String, Path, description = "Unique identifier of the Credential"), + ), responses( (status = 200, description = "Credential found", body = [CredentialView]) - ), - params( - ("id" = u64, Path, description = "Unique identitfier of the Credential"), ) )] #[axum_macros::debug_handler] @@ -57,15 +57,27 @@ pub struct CredentialsEndpointRequest { /// Create a new credential /// -/// Create a new credential for the given payload. +/// Create a new credential for a given subject. #[utoipa::path( post, path = "/credentials", - request_body = CredentialsEndpointRequest, - tag = "Credentials", + request_body(content = CredentialsEndpointRequest, + examples( + ("w3c-vc" = (summary = "W3C v1.1", description = "s0me descr1pti0n", value = json!({"offerId": 123, "credentialConfigurationId": "w3c_vc_credential", "credential": {"credentialSubject": {"first_name": "Ferris", "last_name": "Rustacean"}}}))), + ("openbadges" = (summary = "Open Badges 3.0", description = "s0me descr1pti0n", external_value = "res/open-badge-request.json")) + ) + ), + tag = "Issuance", responses( - (status = 200, description = "Successfully created a new credential.") - ) + (status = 201, description = "Successfully created a new credential.", body = CredentialView, + headers(("Location" = String, description = "URL of the created resource")), + examples( + ("w3c-vc-1-1" = (summary = "W3C VC Data Model v1.1", description = "A credential following the W3C Verifiable Credentials Data Model v1.1", value = json!({"offerId": "0001"}))), + ("openbadges-3-0" = (summary = "Open Badges 3.0", description = "An badge following the Open Badges Specification 3.0", value = json!({"foo": "bar"}))) + ) + ), + (status = 400, description = "Invalid payload.") + ), )] #[axum_macros::debug_handler] pub(crate) async fn credentials( diff --git a/agent_api_rest/src/issuance/offers/mod.rs b/agent_api_rest/src/issuance/offers/mod.rs index c8140f35..2d0abb8c 100644 --- a/agent_api_rest/src/issuance/offers/mod.rs +++ b/agent_api_rest/src/issuance/offers/mod.rs @@ -23,16 +23,16 @@ pub struct OffersEndpointRequest { pub offer_id: String, } -/// Create a new offer +/// Create a new credential offer /// -/// Create a new credential offer for the given ID. +/// Create a new offer for one or more credentials. #[utoipa::path( post, path = "/offers", request_body = OffersEndpointRequest, - tag = "Offers", + tag = "Issuance", responses( - (status = 200, description = "Successfully created a new credential offer.") + (status = 200, description = "Successfully created a new credential offer. Response value is standard-compliant and can be used by identity wallet.", body = String, content_type = "application/x-www-form-urlencoded", example = json!("openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fcredential-issuer.example.com%2F%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22type%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%7D%5D%7D")) ) )] #[axum_macros::debug_handler] diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index abcbae9c..bce7b0ba 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -117,8 +117,9 @@ fn get_base_path() -> Result { tags( (name = "(public)", description = "A collection of endpoints that should be publicly accessible without authentication. They are used to resolve metadata or allow communication with wallets."), (name = "(.well-known)", description = "Well-known endpoints provide metadata about the server."), - (name = "Issuance", description = "Issue credentials to holders that will store them in their wallets.", external_docs(description="Official Documentation", url="https://docs.impierce.com")), - ) + (name = "Issuance", description = "Issue credentials to holders that will store them in their wallets.", external_docs(description="Issuance API Documentation", url="https://docs.impierce.com")), + ), + external_docs(description="Official Documentation", url="https://docs.impierce.com"), )] pub struct ApiDoc; @@ -127,8 +128,9 @@ pub fn patch_generated_openapi(mut openapi: utoipa::openapi::OpenApi) -> utoipa: openapi.info.description = Some("Full HTTP API reference for the UniCore SSI Agent".to_string()); // openapi.info.version = "1.0.0-alpha.1".into(); // can UniCore even be aware of its current version or does it need to be removed from the openapi.yaml? openapi.info.version = "".into(); + // TODO: deploy openapi.servers = vec![ServerBuilder::new() - .url("https://arty-aragorn.agent-dev.impierce.com") + .url("https://playground.agent-dev.impierce.com") .description(Some("UniCore development server hosted by Impierce Technologies")) .build()] .into(); diff --git a/agent_api_rest/src/verification/authorization_requests.rs b/agent_api_rest/src/verification/authorization_requests.rs index 36c1cb49..fa0804a6 100644 --- a/agent_api_rest/src/verification/authorization_requests.rs +++ b/agent_api_rest/src/verification/authorization_requests.rs @@ -27,6 +27,9 @@ use utoipa::ToSchema; get, path = "/authorization_requests/{id}", tag = "Verification", + params( + ("id" = String, Path, description = "The ID of the Authorization Request to retrieve.") + ), responses( (status = 200, description = "Successfully returns an existing Authorization Request.", body = [GenericAuthorizationRequest]) ) diff --git a/agent_api_rest/src/verification/relying_party/redirect.rs b/agent_api_rest/src/verification/relying_party/redirect.rs index 8e6258dd..a61de50b 100644 --- a/agent_api_rest/src/verification/relying_party/redirect.rs +++ b/agent_api_rest/src/verification/relying_party/redirect.rs @@ -18,7 +18,7 @@ use axum::{ path = "/redirect", tags = ["(public)"], responses( - // (status = 200, description = ""), + (status = 200, description = ""), ) )] #[axum_macros::debug_handler] diff --git a/agent_api_rest/src/verification/relying_party/request.rs b/agent_api_rest/src/verification/relying_party/request.rs index 8c892791..a9aa319b 100644 --- a/agent_api_rest/src/verification/relying_party/request.rs +++ b/agent_api_rest/src/verification/relying_party/request.rs @@ -19,7 +19,7 @@ use hyper::header; path = "/request/{id}", tags = ["(public)"], responses( - // (status = 200, description = ""), + (status = 200, description = ""), ) )] #[axum_macros::debug_handler] From 35a8d1197c8d9d2c322ec01e958a3a0ebc31cb03 Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Mon, 30 Sep 2024 01:28:33 +0200 Subject: [PATCH 47/48] docs: add missing endpoints --- agent_api_rest/openapi.yaml | 64 +++++++++++++++++-- .../src/holder/holder/offers/accept.rs | 2 +- .../src/holder/holder/offers/reject.rs | 2 +- agent_api_rest/src/holder/openid4vci/mod.rs | 4 +- agent_api_rest/src/issuance/credentials.rs | 6 +- agent_api_rest/src/issuance/offers/send.rs | 12 ++++ agent_api_rest/src/lib.rs | 7 +- agent_api_rest/src/openapi.rs | 57 ++++++++++++++++- 8 files changed, 139 insertions(+), 15 deletions(-) diff --git a/agent_api_rest/openapi.yaml b/agent_api_rest/openapi.yaml index e779a14d..4260f664 100644 --- a/agent_api_rest/openapi.yaml +++ b/agent_api_rest/openapi.yaml @@ -9,6 +9,40 @@ servers: - url: https://playground.agent-dev.impierce.com description: UniCore development server hosted by Impierce Technologies paths: + /.well-known/did-configuration.json: + get: + tags: + - (.well-known) + - (public) + summary: DID Configuration Resource for Domain Linkage + description: Standard .well-known endpoint for DID Configuration Resources. + operationId: did_configuration_json + responses: + '200': + description: DID Configuration Resource + content: + application/json: + schema: + $ref: '#/components/schemas/DomainLinkageConfiguration' + '404': + description: Domain Linkage inactive. + /.well-known/did.json: + get: + tags: + - (.well-known) + - (public) + summary: DID Document for `did:web` method + description: Standard .well-known endpoint for self-hosted DID Document. + operationId: did_json + responses: + '200': + description: DID Document for `did:web` method + content: + application/json: + schema: + $ref: '#/components/schemas/CoreDocument' + '404': + description: DID method `did:web` inactive. /.well-known/oauth-authorization-server: get: tags: @@ -89,7 +123,10 @@ paths: - Holder - (public) summary: Credential Offer Endpoint - description: Standard OpenID4VCI endpoint that allows the issuer to pass information to the wallet. + description: |- + Standard OpenID4VCI endpoint that allows the Issuer to pass information about the credential offer to the Holder's wallet. + + [Specification](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer-endpoint) operationId: offers requestBody: description: '' @@ -233,9 +270,9 @@ paths: /v0/credentials/{id}: get: tags: - - Credentials + - Issuance summary: Retrieve a credential - description: Return a credential for a given ID. + description: Retrieves an existing credential by its ID. operationId: get_credentials parameters: - name: id @@ -244,6 +281,7 @@ paths: required: true schema: type: string + example: '0001' responses: '200': description: Credential found @@ -278,7 +316,7 @@ paths: tags: - Holder summary: Accept an offer - description: Accepts a pending credential offer. + description: Accept a pending credential offer. UniCore will then make a request to the Issuer to receive the offer. operationId: accept parameters: - name: offer_id @@ -294,7 +332,7 @@ paths: tags: - Holder summary: Reject an offer - description: Reject a pending credential offer. + description: Reject a pending credential offer. UniCore will not make any further requests to the Issuer. operationId: reject parameters: - name: offer_id @@ -326,6 +364,22 @@ paths: schema: type: string example: openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fcredential-issuer.example.com%2F%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22type%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%7D%5D%7D + /v0/offers/send: + post: + tags: + - Issuance + summary: Send offer to Holder + description: Sends a prepared credential offer to a Holder's [Credential Offer Endpoint](#tag/holder/GET/openid4vci/offers) via a `GET` request. + operationId: send + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SendOfferEndpointRequest' + required: true + responses: + '200': + description: Successfully sent credential offer to Holder. components: schemas: CredentialsEndpointRequest: diff --git a/agent_api_rest/src/holder/holder/offers/accept.rs b/agent_api_rest/src/holder/holder/offers/accept.rs index 0bde101c..d7fa2e90 100644 --- a/agent_api_rest/src/holder/holder/offers/accept.rs +++ b/agent_api_rest/src/holder/holder/offers/accept.rs @@ -12,7 +12,7 @@ use hyper::StatusCode; /// Accept an offer /// -/// Accepts a pending credential offer. +/// Accept a pending credential offer. UniCore will then make a request to the Issuer to receive the offer. #[utoipa::path( post, path = "/holder/offers/{offer_id}/accept", diff --git a/agent_api_rest/src/holder/holder/offers/reject.rs b/agent_api_rest/src/holder/holder/offers/reject.rs index c8d8311f..fb1ebd58 100644 --- a/agent_api_rest/src/holder/holder/offers/reject.rs +++ b/agent_api_rest/src/holder/holder/offers/reject.rs @@ -8,7 +8,7 @@ use hyper::StatusCode; /// Reject an offer /// -/// Reject a pending credential offer. +/// Reject a pending credential offer. UniCore will not make any further requests to the Issuer. #[utoipa::path( post, path = "/holder/offers/{offer_id}/reject", diff --git a/agent_api_rest/src/holder/openid4vci/mod.rs b/agent_api_rest/src/holder/openid4vci/mod.rs index ccb4a5c2..133a8760 100644 --- a/agent_api_rest/src/holder/openid4vci/mod.rs +++ b/agent_api_rest/src/holder/openid4vci/mod.rs @@ -18,7 +18,9 @@ pub struct Oid4vciOfferEndpointRequest { /// Credential Offer Endpoint /// -/// Standard OpenID4VCI endpoint that allows the issuer to pass information to the wallet. +/// Standard OpenID4VCI endpoint that allows the Issuer to pass information about the credential offer to the Holder's wallet. +/// +/// [Specification](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer-endpoint) #[utoipa::path( get, path = "/openid4vci/offers", diff --git a/agent_api_rest/src/issuance/credentials.rs b/agent_api_rest/src/issuance/credentials.rs index f66e2342..5f70156d 100644 --- a/agent_api_rest/src/issuance/credentials.rs +++ b/agent_api_rest/src/issuance/credentials.rs @@ -20,13 +20,13 @@ use utoipa::ToSchema; /// Retrieve a credential /// -/// Return a credential for a given ID. +/// Retrieves an existing credential by its ID. #[utoipa::path( get, path = "/credentials/{id}", - tag = "Credentials", + tag = "Issuance", params( - ("id" = String, Path, description = "Unique identifier of the Credential"), + ("id" = String, Path, description = "Unique identifier of the Credential", example = "0001"), ), responses( (status = 200, description = "Credential found", body = [CredentialView]) diff --git a/agent_api_rest/src/issuance/offers/send.rs b/agent_api_rest/src/issuance/offers/send.rs index 2e9a973a..d2fd9a41 100644 --- a/agent_api_rest/src/issuance/offers/send.rs +++ b/agent_api_rest/src/issuance/offers/send.rs @@ -17,6 +17,18 @@ pub struct SendOfferEndpointRequest { pub target_url: Url, } +/// Send offer to Holder +/// +/// Sends a prepared credential offer to a Holder's [Credential Offer Endpoint](#tag/holder/GET/openid4vci/offers) via a `GET` request. +#[utoipa::path( + post, + path = "/offers/send", + request_body = SendOfferEndpointRequest, + tag = "Issuance", + responses( + (status = 200, description = "Successfully sent credential offer to Holder."), + ) +)] #[axum_macros::debug_handler] pub(crate) async fn send(State(state): State, Json(payload): Json) -> Response { info!("Request Body: {}", payload); diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index bce7b0ba..d81ac93a 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -13,7 +13,7 @@ use tracing::{info_span, Span}; use utoipa::{openapi::ServerBuilder, OpenApi}; use utoipa_scalar::{Scalar, Servable}; -use crate::openapi::{HolderApi, IssuanceApi, VerificationApi}; +use crate::openapi::{did_configuration, did_web, HolderApi, IssuanceApi, VerificationApi}; pub const API_VERSION: &str = "/v0"; @@ -134,6 +134,11 @@ pub fn patch_generated_openapi(mut openapi: utoipa::openapi::OpenApi) -> utoipa: .description(Some("UniCore development server hosted by Impierce Technologies")) .build()] .into(); + // Append endpoints defined outside of `agent_api_rest`. + openapi.paths.add_path("/.well-known/did.json", did_web()); + openapi + .paths + .add_path("/.well-known/did-configuration.json", did_configuration()); openapi } diff --git a/agent_api_rest/src/openapi.rs b/agent_api_rest/src/openapi.rs index a2b2a6ce..95293a26 100644 --- a/agent_api_rest/src/openapi.rs +++ b/agent_api_rest/src/openapi.rs @@ -1,3 +1,5 @@ +use utoipa::openapi::path::OperationBuilder; +use utoipa::openapi::{Content, HttpMethod, PathItem, Ref, Response, ResponseBuilder, ResponsesBuilder}; use utoipa::OpenApi; use crate::holder::holder; @@ -7,9 +9,13 @@ use crate::verification::authorization_requests; #[derive(OpenApi)] #[openapi( - // paths(credential::credential, credentials, get_credentials), - paths(credentials::credentials, credentials::get_credentials, offers::offers), - components(schemas(CredentialsEndpointRequest)) + paths( + credentials::credentials, + credentials::get_credentials, + offers::offers, + offers::send::send + ), + components(schemas(CredentialsEndpointRequest)) )] pub(crate) struct IssuanceApi; @@ -28,3 +34,48 @@ pub(crate) struct VerificationApi; holder::offers::reject::reject ))] pub(crate) struct HolderApi; + +pub(crate) fn did_web() -> PathItem { + PathItem::new( + HttpMethod::Get, + OperationBuilder::new() + .responses( + ResponsesBuilder::new() + .response( + "200", + ResponseBuilder::new() + .description("DID Document for `did:web` method") + .content("application/json", Content::new(Ref::from_schema_name("CoreDocument"))), + ) + .response("404", Response::new("DID method `did:web` inactive.")), + ) + .operation_id(Some("did_json")) + .summary(Some("DID Document for `did:web` method")) + .description(Some("Standard .well-known endpoint for self-hosted DID Document.")) + .tags(Some(vec!["(.well-known)", "(public)"])), + ) +} + +pub(crate) fn did_configuration() -> PathItem { + PathItem::new( + HttpMethod::Get, + OperationBuilder::new() + .responses( + ResponsesBuilder::new() + .response( + "200", + ResponseBuilder::new() + .description("DID Configuration Resource") + .content( + "application/json", + Content::new(Ref::from_schema_name("DomainLinkageConfiguration")), + ), + ) + .response("404", Response::new("Domain Linkage inactive.")), + ) + .operation_id(Some("did_configuration_json")) + .summary(Some("DID Configuration Resource for Domain Linkage")) + .description(Some("Standard .well-known endpoint for DID Configuration Resources.")) + .tags(Some(vec!["(.well-known)", "(public)"])), + ) +} From 20d09a59e89d198fe10f826ed88719f0b215557d Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Wed, 2 Oct 2024 23:46:00 +0200 Subject: [PATCH 48/48] docs: include Markdown description, add schemas --- agent_api_rest/docs/openapi-description.md | 58 ++++++++++ agent_api_rest/openapi.yaml | 114 ++++++++++++++++++-- agent_api_rest/src/holder/openid4vci/mod.rs | 4 +- agent_api_rest/src/issuance/credentials.rs | 2 +- agent_api_rest/src/issuance/offers/send.rs | 9 +- agent_api_rest/src/lib.rs | 16 +-- agent_api_rest/src/openapi.rs | 46 +++++--- 7 files changed, 213 insertions(+), 36 deletions(-) create mode 100644 agent_api_rest/docs/openapi-description.md diff --git a/agent_api_rest/docs/openapi-description.md b/agent_api_rest/docs/openapi-description.md new file mode 100644 index 00000000..46d8416b --- /dev/null +++ b/agent_api_rest/docs/openapi-description.md @@ -0,0 +1,58 @@ +![Banner](https://images.placeholders.dev/?width=1280&height=720) + +Full HTTP API reference for the UniCore SSI Agent. + +## Overview + +### Management endpoints + +### Standardized endpoints + +Some endpoints that UniCore offers follow a specification (such as the [OpenID4VC](https://openid.net/sg/openid4vc/specifications) protocol family). These endpoints have the **`(standardized)`** tag. + +### Public endpoints + +Some endpoints should always be publicly accessible to allow identity wallets to interact with UniCore and follow standard protocol flows. These endpoints have the **`(public)`** tag. + +> [!NOTE] +> Endpoints that should not sit behind some form of authentication are grouped under the `(public)` tag. + +```json +{ + "foo": "bar" +} +``` + +## Authentication & Authorization + +UniCore does not have any user management or authentication built-in (yet). It does not know of any roles or scopes. It is expected that the application which calls UniCore only performs calls which have been checked in the consumer business logic. If you want to deploy UniCore publicly, you should restrict access to the API by running it behind a reverse proxy or some API gateway. In most cases, only the endpoints behind `/v0` need to be protected, but all other endpoints should stay publicly accessible. + +### Example reverse proxy configuration + +Here is an example Nginx configuration that restricts access to the `/v0` endpoints by checking for a valid API key in the headers: + +
+ nginx.conf + +``` +http { + server { + listen 8080; + gzip on; + + location /v0 { + if ($http_x_api_key != "A041FE585C6F45CF841D20D47D329FA5") { + return 403; + } + + proxy_pass http://127.0.0.1:3033/v0; + } + + location / { + proxy_pass http://127.0.0.1:3033; + } + } +} +``` + +
diff --git a/agent_api_rest/openapi.yaml b/agent_api_rest/openapi.yaml index 4260f664..1a66112c 100644 --- a/agent_api_rest/openapi.yaml +++ b/agent_api_rest/openapi.yaml @@ -1,13 +1,68 @@ openapi: 3.1.0 info: title: UniCore HTTP API - description: Full HTTP API reference for the UniCore SSI Agent + description: | + ![Banner](https://images.placeholders.dev/?width=1280&height=720) + + Full HTTP API reference for the UniCore SSI Agent. + + ## Overview + + ### Management endpoints + + ### Standardized endpoints + + Some endpoints that UniCore offers follow a specification (such as the [OpenID4VC](https://openid.net/sg/openid4vc/specifications) protocol family). These endpoints have the **`(standardized)`** tag. + + ### Public endpoints + + Some endpoints should always be publicly accessible to allow identity wallets to interact with UniCore and follow standard protocol flows. These endpoints have the **`(public)`** tag. + + > [!NOTE] + > Endpoints that should not sit behind some form of authentication are grouped under the `(public)` tag. + + ```json + { + "foo": "bar" + } + ``` + + ## Authentication & Authorization + + UniCore does not have any user management or authentication built-in (yet). It does not know of any roles or scopes. It is expected that the application which calls UniCore only performs calls which have been checked in the consumer business logic. If you want to deploy UniCore publicly, you should restrict access to the API by running it behind a reverse proxy or some API gateway. In most cases, only the endpoints behind `/v0` need to be protected, but all other endpoints should stay publicly accessible. + + ### Example reverse proxy configuration + + Here is an example Nginx configuration that restricts access to the `/v0` endpoints by checking for a valid API key in the headers: + +
+ nginx.conf + + ``` + http { + server { + listen 8080; + gzip on; + + location /v0 { + if ($http_x_api_key != "A041FE585C6F45CF841D20D47D329FA5") { + return 403; + } + + proxy_pass http://127.0.0.1:3033/v0; + } + + location / { + proxy_pass http://127.0.0.1:3033; + } + } + } + ``` + +
license: name: '' version: '' -servers: -- url: https://playground.agent-dev.impierce.com - description: UniCore development server hosted by Impierce Technologies paths: /.well-known/did-configuration.json: get: @@ -129,10 +184,10 @@ paths: [Specification](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer-endpoint) operationId: offers requestBody: - description: '' content: application/json: - schema: {} + schema: + $ref: '#/components/schemas/Oid4vciOfferEndpointRequest' required: true responses: '200': @@ -235,7 +290,7 @@ paths: summary: W3C v1.1 description: s0me descr1pti0n value: - offerId: 123 + offerId: '123' credentialConfigurationId: w3c_vc_credential credential: credentialSubject: @@ -369,19 +424,41 @@ paths: tags: - Issuance summary: Send offer to Holder - description: Sends a prepared credential offer to a Holder's [Credential Offer Endpoint](#tag/holder/GET/openid4vci/offers) via a `GET` request. + description: |- + Manually send a prepared credential offer to a Holder's [Credential Offer Endpoint](#tag/holder/GET/openid4vci/offers) via a `GET` request. + This is **not** required if the wallet initiates the flow (usually an end-user mobile wallet), but rather when the Holder that has no prior knowledge of the offer (most often another cloud-based wallet, such as another UniCore instance). operationId: send requestBody: content: application/json: schema: $ref: '#/components/schemas/SendOfferEndpointRequest' + example: + offerId: '0001' + targetUrl: https://wallet.example.com/openid4vci/offers required: true responses: '200': description: Successfully sent credential offer to Holder. + '400': + description: Invalid payload. components: schemas: + AuthorizationRequestsEndpointRequest: + allOf: + - allOf: + - type: 'null' + - $ref: '#/components/schemas/PresentationDefinitionResource' + - type: object + required: + - nonce + properties: + nonce: + type: string + state: + type: + - string + - 'null' CredentialsEndpointRequest: type: object required: @@ -396,6 +473,27 @@ components: type: boolean offerId: type: string + OffersEndpointRequest: + type: object + required: + - offerId + properties: + offerId: + type: string + Oid4vciOfferEndpointRequest: + allOf: + - $ref: '#/components/schemas/CredentialOffer' + - type: object + SendOfferEndpointRequest: + type: object + required: + - offerId + - targetUrl + properties: + offerId: + type: string + targetUrl: + $ref: '#/components/schemas/Url' tags: - name: (public) description: A collection of endpoints that should be publicly accessible without authentication. They are used to resolve metadata or allow communication with wallets. diff --git a/agent_api_rest/src/holder/openid4vci/mod.rs b/agent_api_rest/src/holder/openid4vci/mod.rs index 133a8760..310db955 100644 --- a/agent_api_rest/src/holder/openid4vci/mod.rs +++ b/agent_api_rest/src/holder/openid4vci/mod.rs @@ -9,8 +9,9 @@ use hyper::StatusCode; use oid4vci::credential_offer::CredentialOffer; use serde::{Deserialize, Serialize}; use tracing::info; +use utoipa::ToSchema; -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, ToSchema)] pub struct Oid4vciOfferEndpointRequest { #[serde(flatten)] pub credential_offer: CredentialOffer, @@ -24,6 +25,7 @@ pub struct Oid4vciOfferEndpointRequest { #[utoipa::path( get, path = "/openid4vci/offers", + request_body = Oid4vciOfferEndpointRequest, tag = "Holder", tags = ["(public)"], responses( diff --git a/agent_api_rest/src/issuance/credentials.rs b/agent_api_rest/src/issuance/credentials.rs index 5f70156d..9c39bd25 100644 --- a/agent_api_rest/src/issuance/credentials.rs +++ b/agent_api_rest/src/issuance/credentials.rs @@ -63,7 +63,7 @@ pub struct CredentialsEndpointRequest { path = "/credentials", request_body(content = CredentialsEndpointRequest, examples( - ("w3c-vc" = (summary = "W3C v1.1", description = "s0me descr1pti0n", value = json!({"offerId": 123, "credentialConfigurationId": "w3c_vc_credential", "credential": {"credentialSubject": {"first_name": "Ferris", "last_name": "Rustacean"}}}))), + ("w3c-vc" = (summary = "W3C v1.1", description = "s0me descr1pti0n", value = json!({"offerId": "123", "credentialConfigurationId": "w3c_vc_credential", "credential": {"credentialSubject": {"first_name": "Ferris", "last_name": "Rustacean"}}}))), ("openbadges" = (summary = "Open Badges 3.0", description = "s0me descr1pti0n", external_value = "res/open-badge-request.json")) ) ), diff --git a/agent_api_rest/src/issuance/offers/send.rs b/agent_api_rest/src/issuance/offers/send.rs index d2fd9a41..10f9b0a4 100644 --- a/agent_api_rest/src/issuance/offers/send.rs +++ b/agent_api_rest/src/issuance/offers/send.rs @@ -9,8 +9,9 @@ use hyper::StatusCode; use serde::{Deserialize, Serialize}; use tracing::info; use url::Url; +use utoipa::ToSchema; -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct SendOfferEndpointRequest { pub offer_id: String, @@ -19,14 +20,16 @@ pub struct SendOfferEndpointRequest { /// Send offer to Holder /// -/// Sends a prepared credential offer to a Holder's [Credential Offer Endpoint](#tag/holder/GET/openid4vci/offers) via a `GET` request. +/// Manually send a prepared credential offer to a Holder's [Credential Offer Endpoint](#tag/holder/GET/openid4vci/offers) via a `GET` request. +/// This is **not** required if the wallet initiates the flow (usually an end-user mobile wallet), but rather when the Holder that has no prior knowledge of the offer (most often another cloud-based wallet, such as another UniCore instance). #[utoipa::path( post, path = "/offers/send", - request_body = SendOfferEndpointRequest, + request_body(content = SendOfferEndpointRequest, example = json!({"offerId": "0001", "targetUrl": "https://wallet.example.com/openid4vci/offers"})), tag = "Issuance", responses( (status = 200, description = "Successfully sent credential offer to Holder."), + (status = 400, description = "Invalid payload."), ) )] #[axum_macros::debug_handler] diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index d81ac93a..631ac60a 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -10,7 +10,7 @@ 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 utoipa::{openapi::ServerBuilder, OpenApi}; +use utoipa::OpenApi; use utoipa_scalar::{Scalar, Servable}; use crate::openapi::{did_configuration, did_web, HolderApi, IssuanceApi, VerificationApi}; @@ -125,15 +125,15 @@ pub struct ApiDoc; pub fn patch_generated_openapi(mut openapi: utoipa::openapi::OpenApi) -> utoipa::openapi::OpenApi { openapi.info.title = "UniCore HTTP API".into(); - openapi.info.description = Some("Full HTTP API reference for the UniCore SSI Agent".to_string()); + openapi.info.description = Some(include_str!("../docs/openapi-description.md").into()); // openapi.info.version = "1.0.0-alpha.1".into(); // can UniCore even be aware of its current version or does it need to be removed from the openapi.yaml? openapi.info.version = "".into(); - // TODO: deploy - openapi.servers = vec![ServerBuilder::new() - .url("https://playground.agent-dev.impierce.com") - .description(Some("UniCore development server hosted by Impierce Technologies")) - .build()] - .into(); + // TODO: required to use `UNICORE__URL` as the "self" server? + // openapi.servers = vec![ServerBuilder::new() + // .url("https://playground.agent-dev.impierce.com") + // .description(Some("UniCore development server hosted by Impierce Technologies")) + // .build()] + // .into(); // Append endpoints defined outside of `agent_api_rest`. openapi.paths.add_path("/.well-known/did.json", did_web()); openapi diff --git a/agent_api_rest/src/openapi.rs b/agent_api_rest/src/openapi.rs index 95293a26..36a9dcf7 100644 --- a/agent_api_rest/src/openapi.rs +++ b/agent_api_rest/src/openapi.rs @@ -1,9 +1,10 @@ -use utoipa::openapi::path::OperationBuilder; -use utoipa::openapi::{Content, HttpMethod, PathItem, Ref, Response, ResponseBuilder, ResponsesBuilder}; +use utoipa::openapi::{ + path::OperationBuilder, Content, HttpMethod, PathItem, Ref, Response, ResponseBuilder, ResponsesBuilder, +}; use utoipa::OpenApi; -use crate::holder::holder; -use crate::issuance::credentials::{self, CredentialsEndpointRequest}; +use crate::holder::{holder, openid4vci}; +use crate::issuance::credentials; use crate::issuance::offers; use crate::verification::authorization_requests; @@ -15,24 +16,34 @@ use crate::verification::authorization_requests; offers::offers, offers::send::send ), - components(schemas(CredentialsEndpointRequest)) + components(schemas( + credentials::CredentialsEndpointRequest, + offers::OffersEndpointRequest, + offers::send::SendOfferEndpointRequest + )) )] pub(crate) struct IssuanceApi; #[derive(OpenApi)] -#[openapi(paths( - authorization_requests::authorization_requests, - authorization_requests::get_authorization_requests -))] +#[openapi( + paths( + authorization_requests::authorization_requests, + authorization_requests::get_authorization_requests + ), + components(schemas(authorization_requests::AuthorizationRequestsEndpointRequest)) +)] pub(crate) struct VerificationApi; #[derive(OpenApi)] -#[openapi(paths( - holder::credentials::credentials, - holder::offers::offers, - holder::offers::accept::accept, - holder::offers::reject::reject -))] +#[openapi( + paths( + holder::credentials::credentials, + holder::offers::offers, + holder::offers::accept::accept, + holder::offers::reject::reject + ), + components(schemas(openid4vci::Oid4vciOfferEndpointRequest)) +)] pub(crate) struct HolderApi; pub(crate) fn did_web() -> PathItem { @@ -69,6 +80,11 @@ pub(crate) fn did_configuration() -> PathItem { .content( "application/json", Content::new(Ref::from_schema_name("DomainLinkageConfiguration")), + // Content::new( + // ObjectBuilder::new() + // .schema_type(SchemaType::Type(schema::Type::Object)) + // .format(Some(schema::SchemaFormat::KnownFormat(schema::KnownFormat::Int64))), + // ), ), ) .response("404", Response::new("Domain Linkage inactive.")),