From f41c98c73bf234f258bcbb49dbb12e5ef972260b Mon Sep 17 00:00:00 2001 From: ThetaSinner Date: Wed, 28 Feb 2024 01:48:48 +0000 Subject: [PATCH] Remove key from collection without validation --- .../coordinator/trusted/src/key_collection.rs | 102 +++++++++++++++++- .../zomes/integrity/trusted/src/lib.rs | 10 +- .../trusted/trusted/key-collection.test.ts | 56 ++++++++++ 3 files changed, 161 insertions(+), 7 deletions(-) diff --git a/dnas/trusted/zomes/coordinator/trusted/src/key_collection.rs b/dnas/trusted/zomes/coordinator/trusted/src/key_collection.rs index beb9c87..89e1d7c 100644 --- a/dnas/trusted/zomes/coordinator/trusted/src/key_collection.rs +++ b/dnas/trusted/zomes/coordinator/trusted/src/key_collection.rs @@ -83,7 +83,6 @@ pub struct LinkGpgKeyToKeyCollectionRequest { pub key_collection_name: String, } -// TODO prevent duplicate links? Could also be done in the UI fairly easily #[hdk_extern] pub fn link_gpg_key_to_key_collection( request: LinkGpgKeyToKeyCollectionRequest, @@ -147,6 +146,107 @@ pub fn link_gpg_key_to_key_collection( ) } +#[derive(Serialize, Deserialize, Debug, Clone, SerializedBytes)] +pub struct UnlinkGpgKeyFromKeyCollectionRequest { + pub gpg_key_fingerprint: String, + pub key_collection_name: String, +} + +#[hdk_extern] +pub fn unlink_gpg_key_from_key_collection( + request: UnlinkGpgKeyFromKeyCollectionRequest, +) -> ExternResult<()> { + let fingerprint_links = get_links( + GetLinksInputBuilder::try_new( + make_base_hash(&request.gpg_key_fingerprint)?, + LinkTypes::FingerprintToGpgKeyDist, + )? + .build(), + )?; + + if fingerprint_links.is_empty() { + return Err(wasm_error!(WasmErrorInner::Guest(String::from( + "No GPG key found with the given fingerprint" + )))); + } + + if fingerprint_links.len() > 1 { + return Err(wasm_error!(WasmErrorInner::Guest(String::from( + "Multiple GPG keys found with the given fingerprint" + )))); + } + + // Target is the entry hash of the GpgKeyDist + let fingerprint_link = fingerprint_links[0].clone(); + + let my_key_collections = inner_get_my_key_collections()?; + + let matched_collection = my_key_collections.into_iter().find(|r| { + r.entry + .as_option() + .and_then(|e| e.as_app_entry()) + .and_then(|e| { + let key_collection: KeyCollection = e.clone().into_sb().try_into().ok()?; + Some(key_collection.name == request.key_collection_name) + }) + .unwrap_or(false) + }); + + let key_collection = matched_collection.ok_or(wasm_error!(WasmErrorInner::Guest( + String::from("No key collection found with the given name") + )))?; + + let agent_info = agent_info()?; + + let potential_links_from_selected_collection = get_links( + GetLinksInputBuilder::try_new( + key_collection.action_hashed().as_hash().clone(), + LinkTypes::KeyCollectionToGpgKeyDist.try_into_filter()?, + )? + .author(agent_info.agent_initial_pubkey.clone()) + .build(), + )?; + + // Unlink the the key collection from the GpgKeyDist + let mut removing_tags = HashSet::new(); + for link in potential_links_from_selected_collection.into_iter() { + // Find the links from this collection that target the key to remove + if link.target == fingerprint_link.target.clone() { + removing_tags.insert(link.tag); + delete_link(link.create_link_hash)?; + } + } + + let potential_links_from_fingerprint = get_links( + GetLinksInputBuilder::try_new( + fingerprint_link.target, + LinkTypes::GpgKeyDistToKeyCollection.try_into_filter()?, + )? + .author(agent_info.agent_initial_pubkey) + .build(), + )?; + + // Unlink the key fingerprint from the key collection + for link in potential_links_from_fingerprint.into_iter() { + // Find the links from this collection that target the key to remove + if link.target == key_collection.action_hashed().as_hash().clone().into() { + if !removing_tags.remove(&link.tag) { + return Err(wasm_error!(WasmErrorInner::Guest(format!( + "Link from fingerprint to key collection has tag {:?} but no corresponding link from key collection to fingerprint was deleted", + link.tag + )))); + } + delete_link(link.create_link_hash)?; + } + } + + if !removing_tags.is_empty() { + tracing::warn!("There were links from the key collection that did not correspond to a link from the key fingerprint. Validation is supposed to prevent this. {:?}", removing_tags); + } + + Ok(()) +} + /// Checks if the key collection can be created. /// /// - Ensures the name is at least [KEY_COLLECTION_NAME_MIN_LENGTH] characters long. Also checked by validation. diff --git a/dnas/trusted/zomes/integrity/trusted/src/lib.rs b/dnas/trusted/zomes/integrity/trusted/src/lib.rs index 6ef788e..850e566 100644 --- a/dnas/trusted/zomes/integrity/trusted/src/lib.rs +++ b/dnas/trusted/zomes/integrity/trusted/src/lib.rs @@ -161,9 +161,9 @@ pub fn validate(op: Op) -> ExternResult { ) } }, - FlatOp::RegisterDeleteLink { .. } => Ok(ValidateCallbackResult::Invalid(String::from( - "There are no link types in this integrity zome", - ))), + FlatOp::RegisterDeleteLink { original_action, link_type, tag, .. } => match link_type { + _ => Ok(ValidateCallbackResult::Valid) + }, FlatOp::StoreRecord(store_record) => { match store_record { // Complementary validation to the `StoreEntry` Op, in which the record itself is validated @@ -337,9 +337,7 @@ pub fn validate(op: Op) -> ExternResult { // Complementary validation to the `RegisterDeleteLink` Op, in which the record itself is validated // If you want to optimize performance, you can remove the validation for an entry type here and keep it in `RegisterDeleteLink` // Notice that doing so will cause `must_get_valid_record` for this record to return a valid record even if the `RegisterDeleteLink` validation failed - OpRecord::DeleteLink { .. } => Ok(ValidateCallbackResult::Invalid( - "There are no link types in this integrity zome".to_string(), - )), + OpRecord::DeleteLink { .. } => Ok(ValidateCallbackResult::Valid), OpRecord::CreatePrivateEntry { .. } => Ok(ValidateCallbackResult::Valid), OpRecord::UpdatePrivateEntry { .. } => Ok(ValidateCallbackResult::Valid), OpRecord::CreateCapClaim { .. } => Ok(ValidateCallbackResult::Valid), diff --git a/tests/src/trusted/trusted/key-collection.test.ts b/tests/src/trusted/trusted/key-collection.test.ts index f34e224..56e7830 100644 --- a/tests/src/trusted/trusted/key-collection.test.ts +++ b/tests/src/trusted/trusted/key-collection.test.ts @@ -122,6 +122,62 @@ test("Link GPG key to collection", async () => { }); }); +test("Unlink GPG key from collection", async () => { + await runScenario(async (scenario) => { + const testAppPath = process.cwd() + "/../workdir/hWOT.happ"; + const appSource = { appBundleSource: { path: testAppPath } }; + + const [alice] = await scenario.addPlayersWithApps([appSource]); + + // Alice distributes a GPG key + const gpg_key_record: Record = await distributeGpgKey( + alice.cells[0], + sampleGpgKey(), + ); + assert.ok(gpg_key_record); + + // Alice creates a key collection + const key_collection_record: Record = await createKeyCollection( + alice.cells[0], + "a test", + ); + assert.ok(key_collection_record); + + // Alice links the GPG key to the key collection + await alice.cells[0].callZome({ + zome_name: "trusted", + fn_name: "link_gpg_key_to_key_collection", + payload: { + gpg_key_fingerprint: + decodeRecord(gpg_key_record).fingerprint, + key_collection_name: "a test", + }, + }); + + // Alice unlinks the GPG key from the key collection + await alice.cells[0].callZome({ + zome_name: "trusted", + fn_name: "unlink_gpg_key_from_key_collection", + payload: { + gpg_key_fingerprint: + decodeRecord(gpg_key_record).fingerprint, + key_collection_name: "a test", + }, + }); + + // Now getting key collections should return a single, empty key collection + const key_collections: KeyCollectionWithKeys[] = + await alice.cells[0].callZome({ + zome_name: "trusted", + fn_name: "get_my_key_collections", + payload: null, + }); + + assert.equal(key_collections.length, 1); + assert.equal(key_collections[0].gpg_keys.length, 0); + }); +}); + test("Remote validation", async () => { await runScenario(async (scenario) => { const testAppPath = process.cwd() + "/../workdir/hWOT.happ";