From 20816255966bad0dd943d788df75784731eba8d1 Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Tue, 26 Mar 2024 14:50:55 +0100 Subject: [PATCH] feat(dataverse): support claim with named node hierarchy --- contracts/okp4-dataverse/src/registrar/rdf.rs | 67 ++++++++++++++----- packages/okp4-rdf/src/normalize.rs | 6 +- 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/contracts/okp4-dataverse/src/registrar/rdf.rs b/contracts/okp4-dataverse/src/registrar/rdf.rs index f7a7d0cd..9cac236a 100644 --- a/contracts/okp4-dataverse/src/registrar/rdf.rs +++ b/contracts/okp4-dataverse/src/registrar/rdf.rs @@ -56,8 +56,11 @@ impl<'a> DataverseCredential<'a> { let claim_node = BlankNode { id: "c0" }; // Used to rename all blank nodes to avoid conflict with the forged claim node `c0` - let mut id_issuer = IdentifierIssuer::new("b", 0u128); - let triples: Vec> = self.as_triples(claim_node, &mut id_issuer)?; + let mut blank_issuer = IdentifierIssuer::new("b", 0u128); + // Used to replace named node based hierarchy with blank nodes + let mut named_issuer = IdentifierIssuer::new("a", 0u128); + let triples: Vec> = + self.as_triples(claim_node, &mut named_issuer, &mut blank_issuer)?; let out: Vec = Vec::default(); let mut writer = TripleWriter::new(&format, out); for triple in triples { @@ -74,7 +77,8 @@ impl<'a> DataverseCredential<'a> { fn as_triples( &'a self, claim_node: BlankNode<'a>, - id_issuer: &'a mut IdentifierIssuer, + named_issuer: &'a mut IdentifierIssuer, + blank_issuer: &'a mut IdentifierIssuer, ) -> Result>, ContractError> { let c_subject = Subject::NamedNode(NamedNode { iri: self.id }); @@ -111,7 +115,7 @@ impl<'a> DataverseCredential<'a> { }, ]; - triples.extend(self.claim_as_triples(claim_node, id_issuer)?); + triples.extend(self.claim_as_triples(claim_node, named_issuer, blank_issuer)?); if let Some(valid_until) = self.valid_until { triples.push(Triple { @@ -130,15 +134,23 @@ impl<'a> DataverseCredential<'a> { fn claim_as_triples( &'a self, claim_node: BlankNode<'a>, - id_issuer: &'a mut IdentifierIssuer, + named_issuer: &'a mut IdentifierIssuer, + blank_issuer: &'a mut IdentifierIssuer, ) -> Result>, ContractError> { - // issue replacement identifiers for blank nodes + // issue replacement identifiers for nodes self.claim.content.iter().for_each(|q| { - if let Subject::BlankNode(BlankNode { id }) = q.subject { - let _ = id_issuer.get_or_issue(id.to_string()); - } + match q.subject { + Subject::NamedNode(NamedNode { iri }) if iri != self.claim.id => { + named_issuer.get_or_issue(iri.to_string()); + } + Subject::BlankNode(BlankNode { id }) => { + blank_issuer.get_or_issue(id.to_string()); + } + _ => (), + }; + if let Term::BlankNode(BlankNode { id }) = q.object { - let _ = id_issuer.get_or_issue(id.to_string()); + blank_issuer.get_or_issue(id.to_string()); } }); @@ -148,16 +160,21 @@ impl<'a> DataverseCredential<'a> { .iter() .map(|q| { let subject = match q.subject { - Subject::NamedNode(n) => { - if n.iri != self.claim.id { - Err(ContractError::UnsupportedCredential( - "claim hierarchy can be forge only through blank nodes".to_string(), - ))?; - } + Subject::NamedNode(n) if n.iri == self.claim.id => { Subject::BlankNode(claim_node) } + Subject::NamedNode(n) if n.iri != self.claim.id => { + Subject::BlankNode(BlankNode { + id: named_issuer.get(n.iri).ok_or_else(|| { + ContractError::Unexpected( + "Could not replace named node, canonical identifier not found" + .to_string(), + ) + })?, + }) + } Subject::BlankNode(BlankNode { id }) => Subject::BlankNode(BlankNode { - id: id_issuer.get(id).ok_or_else(|| { + id: blank_issuer.get(id).ok_or_else(|| { ContractError::Unexpected( "Could not replace blank node, canonical identifier not found" .to_string(), @@ -167,8 +184,12 @@ impl<'a> DataverseCredential<'a> { _ => q.subject, }; let object = match q.object { + Term::NamedNode(n) => match named_issuer.get(n.iri) { + Some(id) => Term::BlankNode(BlankNode { id }), + None => Term::NamedNode(n), + }, Term::BlankNode(BlankNode { id }) => Term::BlankNode(BlankNode { - id: id_issuer.get(id).ok_or_else(|| { + id: blank_issuer.get(id).ok_or_else(|| { ContractError::Unexpected( "Could not replace blank node, canonical identifier not found" .to_string(), @@ -186,6 +207,16 @@ impl<'a> DataverseCredential<'a> { }) .collect::>, ContractError>>()?; + named_issuer + .issued_iter() + .for_each(|(original, (_, replacement))| { + triples.push(Triple { + subject: Subject::BlankNode(BlankNode { id: replacement }), + predicate: VC_CLAIM_ORIGINAL_NODE, + object: Term::NamedNode(NamedNode { iri: original }), + }); + }); + triples.push(Triple { subject: Subject::NamedNode(NamedNode { iri: self.id }), predicate: VC_CLAIM, diff --git a/packages/okp4-rdf/src/normalize.rs b/packages/okp4-rdf/src/normalize.rs index b9eb1541..3291f5cd 100644 --- a/packages/okp4-rdf/src/normalize.rs +++ b/packages/okp4-rdf/src/normalize.rs @@ -2,7 +2,7 @@ use itertools::Itertools; use rio_api::model::{BlankNode, GraphName, Quad, Subject, Term}; use sha2; use sha2::Digest; -use std::collections::hash_map::Entry; +use std::collections::hash_map::{Entry, Iter}; use std::collections::{BTreeMap, HashMap}; use thiserror::Error; @@ -377,6 +377,10 @@ impl IdentifierIssuer { pub fn issued(&self, identifier: &str) -> bool { self.issued.contains_key(identifier) } + + pub fn issued_iter(&self) -> Iter<'_, String, (u128, String)> { + self.issued.iter() + } } trait WithBlankNodes<'a> {