diff --git a/Cargo.lock b/Cargo.lock index 67215a19..9907b0fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2137,6 +2137,7 @@ dependencies = [ "pallet-balances", "pallet-bounties", "pallet-collective", + "pallet-confidential-docs", "pallet-fruniques", "pallet-gated-marketplace", "pallet-grandpa", @@ -4005,6 +4006,20 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-confidential-docs" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", +] + [[package]] name = "pallet-fruniques" version = "0.1.0-dev" diff --git a/pallets/confidential-docs/Cargo.toml b/pallets/confidential-docs/Cargo.toml new file mode 100644 index 00000000..e0865153 --- /dev/null +++ b/pallets/confidential-docs/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "pallet-confidential-docs" +version = "4.0.0-dev" +description = "Provides backend services for the confidentials docs solution" +authors = ["Hashed Pallet { + + pub fn do_set_vault(owner: T::AccountId, user_id: UserId, public_key: PublicKey, cid: CID) -> DispatchResult { + Self::validate_cid(&cid)?; + let hashed_account = owner.using_encoded(blake2_256); + if let Some(uid) = >::get(&hashed_account){ + ensure!(uid == user_id, >::NotOwnerOfUserId); + } else { + ensure!(!>::contains_key(&user_id), >::NotOwnerOfVault); + } + + let vault = Vault{ + cid: cid.clone(), + owner: owner.clone() + }; + >::insert(user_id, vault.clone()); + >::insert(owner.clone(), public_key); + >::insert(hashed_account.clone(), user_id); + + Self::deposit_event(Event::VaultStored(user_id, public_key, vault)); + Ok(()) + } + + pub fn do_set_owned_document(owner: T::AccountId, mut owned_doc: OwnedDoc) -> DispatchResult { + owned_doc.owner = owner.clone(); + Self::validate_owned_doc(&owned_doc)?; + let OwnedDoc { + cid, + .. + } = owned_doc.clone(); + if let Some(doc) = >::get(&cid) { + ensure!(doc.owner == owner, >::NotDocOwner); + } else { + >::try_mutate(&owner, |owned_vec| { + owned_vec.try_push(cid.clone()) + }).map_err(|_| >::ExceedMaxOwnedDocs)?; + } + >::insert(cid.clone(), owned_doc.clone()); + Self::deposit_event(Event::OwnedDocStored(owned_doc)); + Ok(()) + } + + pub fn do_remove_owned_document(owner: T::AccountId, cid: CID) -> DispatchResult { + let doc = >::try_get(&cid).map_err(|_| >::DocNotFound)?; + ensure!(doc.owner == owner, >::NotDocOwner); + >::try_mutate(&owner, |owned_vec| { + let cid_index = owned_vec.iter().position(|c| *c==cid).ok_or(>::CIDNotFound)?; + owned_vec.remove(cid_index); + Ok(()) + }).map_err(|_:Error::| >::CIDNotFound)?; + >::remove(cid.clone()); + Self::deposit_event(Event::OwnedDocRemoved(doc)); + Ok(()) + } + + pub fn do_share_document(owner: T::AccountId, mut shared_doc: SharedDoc) -> DispatchResult { + shared_doc.from = owner; + Self::validate_shared_doc(&shared_doc)?; + let SharedDoc { + cid, + to, + from, + .. + } = shared_doc.clone(); + + >::try_mutate(&from, |shared_vec| { + shared_vec.try_push(cid.clone()) + }).map_err(|_| >::ExceedMaxSharedFromDocs)?; + + >::try_mutate(&to, |shared_vec| { + shared_vec.try_push(cid.clone()) + }).map_err(|_| >::ExceedMaxSharedToDocs)?; + + >::insert(cid.clone(), shared_doc.clone()); + Self::deposit_event(Event::SharedDocStored(shared_doc)); + Ok(()) + } + + pub fn do_update_shared_document_metadata(to: T::AccountId, mut shared_doc: SharedDoc) -> DispatchResult { + let doc = >::try_get(&shared_doc.cid).map_err(|_| >::DocNotFound)?; + ensure!(doc.to == to, >::NotDocSharee); + shared_doc.from = doc.from; + shared_doc.to = to; + >::insert(doc.cid.clone(), shared_doc.clone()); + Self::deposit_event(Event::SharedDocUpdated(shared_doc)); + Ok(()) + } + + pub fn do_remove_shared_document(to: T::AccountId, cid: CID) -> DispatchResult { + let doc = >::try_get(&cid).map_err(|_| >::DocNotFound)?; + ensure!(doc.to == to, >::NotDocSharee); + >::try_mutate(&to, |shared_vec| { + let cid_index = shared_vec.iter().position(|c| *c==cid).ok_or(>::CIDNotFound)?; + shared_vec.remove(cid_index); + Ok(()) + }).map_err(|_:Error::| >::CIDNotFound)?; + >::try_mutate(&doc.from, |shared_vec| { + let cid_index = shared_vec.iter().position(|c| *c==cid).ok_or(>::CIDNotFound)?; + shared_vec.remove(cid_index); + Ok(()) + }).map_err(|_:Error::| >::CIDNotFound)?; + >::remove(cid.clone()); + Self::deposit_event(Event::SharedDocRemoved(doc)); + Ok(()) + } + + fn validate_owned_doc(owned_doc: &OwnedDoc)->DispatchResult{ + let OwnedDoc { + cid, + name, + description, + owner + } = owned_doc; + Self::validate_cid(cid)?; + Self::validate_doc_name(name)?; + Self::validate_doc_desc(description)?; + Self::validate_has_public_key(owner)?; + Ok(()) + } + + fn validate_shared_doc(shared_doc: &SharedDoc)->DispatchResult{ + let SharedDoc { + cid, + name, + description, + from, + to, + } = shared_doc; + Self::validate_cid(cid)?; + Self::validate_doc_name(name)?; + Self::validate_doc_desc(description)?; + ensure!(from != to, >::DocSharedWithSelf); + ensure!(!>::contains_key(cid), >::DocAlreadyShared); + Self::validate_has_public_key(from)?; + Self::validate_has_public_key(to)?; + Ok(()) + } + + fn validate_has_public_key(who: &T::AccountId)->DispatchResult{ + ensure!(>::contains_key(who), >::AccountHasNoPublicKey); + Ok(()) + } + fn validate_cid(cid: &CID)->DispatchResult{ + ensure!(cid.len() > 0, >::CIDNoneValue); + Ok(()) + } + + fn validate_doc_name(doc_name: &DocName)->DispatchResult{ + ensure!(doc_name.len() >= T::DocNameMinLen::get().try_into().unwrap(), >::DocNameTooShort); + Ok(()) + } + + fn validate_doc_desc(doc_desc: &DocDesc)->DispatchResult{ + ensure!(doc_desc.len() >= T::DocDescMinLen::get().try_into().unwrap(), >::DocDescTooShort); + Ok(()) + } +} \ No newline at end of file diff --git a/pallets/confidential-docs/src/lib.rs b/pallets/confidential-docs/src/lib.rs new file mode 100644 index 00000000..ffc134cc --- /dev/null +++ b/pallets/confidential-docs/src/lib.rs @@ -0,0 +1,322 @@ +//! The confidential docs pallet provides the backend services and metadata +//! storage for the confidential docs solution + +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +// #[cfg(feature = "runtime-benchmarks")] +// mod benchmarking; + +pub mod types; +mod functions; + +#[frame_support::pallet] +pub mod pallet { + //! Provides the backend services and metadata storage for the confidential docs solution + use frame_support::{pallet_prelude::*, transactional}; + use frame_system::pallet_prelude::*; + use crate::types::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + + type Event: From> + IsType<::Event>; + + type RemoveOrigin: EnsureOrigin; + + /// Maximum number of confidential documents that a user can own + #[pallet::constant] + type MaxOwnedDocs: Get; + /// Maximum number of confidential documents that a user can share + #[pallet::constant] + type MaxSharedFromDocs: Get; + /// Maximum number of confidential documents that can be shared to a user + #[pallet::constant] + type MaxSharedToDocs: Get; + /// Minimum length for a document name + #[pallet::constant] + type DocNameMinLen: Get; + /// Maximum length for a document name + #[pallet::constant] + type DocNameMaxLen: Get; + /// Minimum length for a document description + #[pallet::constant] + type DocDescMinLen: Get; + /// Maximum length for a document description + #[pallet::constant] + type DocDescMaxLen: Get; + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + + #[pallet::storage] + #[pallet::getter(fn vaults)] + pub(super) type Vaults = StorageMap< + _, + Blake2_256, + UserId, //user identifier + Vault, + OptionQuery + >; + + #[pallet::storage] + #[pallet::getter(fn public_keys)] + pub(super) type PublicKeys = StorageMap< + _, + Blake2_256, + T::AccountId, + PublicKey, + OptionQuery + >; + + #[pallet::storage] + #[pallet::getter(fn users_ids)] + pub(super) type UserIds = StorageMap< + _, + Identity, + [u8; 32], + UserId, + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn owned_docs)] + pub(super) type OwnedDocs = StorageMap< + _, + Blake2_256, + CID, + OwnedDoc, + OptionQuery + >; + + #[pallet::storage] + #[pallet::getter(fn owned_docs_by_owner)] + pub(super) type OwnedDocsByOwner = StorageMap< + _, + Blake2_256, + T::AccountId, + BoundedVec, + ValueQuery + >; + + #[pallet::storage] + #[pallet::getter(fn shared_docs)] + pub(super) type SharedDocs = StorageMap< + _, + Blake2_256, + CID, + SharedDoc, + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn shared_docs_by_to)] + pub(super) type SharedDocsByTo = StorageMap< + _, + Blake2_256, + T::AccountId, + BoundedVec, + ValueQuery + >; + + #[pallet::storage] + #[pallet::getter(fn shared_docs_by_from)] + pub(super) type SharedDocsByFrom = StorageMap< + _, + Blake2_256, + T::AccountId, + BoundedVec, + ValueQuery + >; + + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Vault stored + VaultStored(UserId, PublicKey, Vault), + /// Owned confidential document stored + OwnedDocStored(OwnedDoc), + /// Owned confidential document removed + OwnedDocRemoved(OwnedDoc), + /// Shared confidential document stored + SharedDocStored(SharedDoc), + /// Shared confidential document metadata updated + SharedDocUpdated(SharedDoc), + /// Shared confidential document removed + SharedDocRemoved(SharedDoc), + } + + #[pallet::error] + pub enum Error { + /// Empty CID + CIDNoneValue, + /// Document Name is too short + DocNameTooShort, + /// Document Desc is too short + DocDescTooShort, + /// Errors should have helpful documentation associated with them. + StorageOverflow, + /// Origin is not the owner of the user id + NotOwnerOfUserId, + /// Origin is not the owner of the vault + NotOwnerOfVault, + /// The user already has a vault + UserAlreadyHasVault, + /// The user already has a public key + AccountAlreadyHasPublicKey, + /// User is not document owner + NotDocOwner, + /// User is not document whom with the document was shared + NotDocSharee, + /// CID not found + CIDNotFound, + /// Document not found + DocNotFound, + /// The document has already been shared + DocAlreadyShared, + /// Shared with self + DocSharedWithSelf, + /// Account has no public key + AccountHasNoPublicKey, + /// Max owned documents has been exceeded + ExceedMaxOwnedDocs, + /// Max documents shared with the "to" account has been exceeded + ExceedMaxSharedToDocs, + /// Max documents shared with the "from" account has been exceeded + ExceedMaxSharedFromDocs, + + + } + + #[pallet::call] + impl Pallet { + + /// Create/Update a vault + /// + /// Creates/Updates the calling user's vault and sets their public cipher key + /// . + /// ### Parameters: + /// - `origin`: The user that is configuring their vault + /// - `user_id`: User identifier generated from their login method, their address if using + /// native login or user id if using SSO + /// - `public key`: The users cipher public key + /// - `cid`: The IPFS CID that contains the vaults data + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn set_vault(origin: OriginFor, user_id: UserId, public_key: PublicKey, cid: CID) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_set_vault(who, user_id, public_key, cid) + } + + /// Create/Update an owned document + /// + /// Creates a new owned document or updates an existing owned document's metadata + /// . + /// ### Parameters: + /// - `origin`: The user that is creating/updating an owned document + /// - `owned_doc`: Metadata related to the owned document + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn set_owned_document(origin: OriginFor, owned_doc: OwnedDoc) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_set_owned_document(who, owned_doc) + } + + /// Remove an owned document + /// + /// Removes an owned document + /// . + /// ### Parameters: + /// - `origin`: The owner of the document + /// - `cid`: of the document to be removed + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn remove_owned_document(origin: OriginFor, cid: CID) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_remove_owned_document(who, cid) + } + + /// Share a document + /// + /// Creates a shared document + /// . + /// ### Parameters: + /// - `origin`: The user that is creating the shared document + /// - `shared_doc`: Metadata related to the shared document + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn share_document(origin: OriginFor, shared_doc: SharedDoc) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_share_document(who, shared_doc) + } + + /// Update share document metadata + /// + /// Updates share document metadata, only the user with which the document + /// was shared can update it + /// . + /// ### Parameters: + /// - `origin`: The "to" user of the shared document + /// - `shared_doc`: Metadata related to the shared document + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn update_shared_document_metadata(origin: OriginFor, shared_doc: SharedDoc) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_update_shared_document_metadata(who, shared_doc) + } + + + /// Remove a shared document + /// + /// Removes a shared document, only the user with whom the document was + /// is able to remove it + /// . + /// ### Parameters: + /// - `origin`: The "to" user of the shared document + /// - `cid`: of the document to be removed + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn remove_shared_document(origin: OriginFor, cid: CID) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_remove_shared_document(who, cid) + } + + + /// Kill all the stored data. + /// + /// This function is used to kill ALL the stored data. + /// Use with caution! + /// + /// ### Parameters: + /// - `origin`: The user who performs the action. + /// + /// ### Considerations: + /// - This function is only available to the `admin` with sudo access. + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn kill_storage( + origin: OriginFor, + ) -> DispatchResult{ + T::RemoveOrigin::ensure_origin(origin.clone())?; + >::remove_all(None); + >::remove_all(None); + >::remove_all(None); + >::remove_all(None); + >::remove_all(None); + >::remove_all(None); + >::remove_all(None); + Ok(()) + } + } +} \ No newline at end of file diff --git a/pallets/confidential-docs/src/mock.rs b/pallets/confidential-docs/src/mock.rs new file mode 100644 index 00000000..4919dc61 --- /dev/null +++ b/pallets/confidential-docs/src/mock.rs @@ -0,0 +1,86 @@ +use crate as pallet_confidential_docs; +use frame_support::parameter_types; +use frame_system as system; +use frame_system::EnsureRoot; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, +}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + ConfidentialDocs: pallet_confidential_docs::{Pallet, Call, Storage, Event}, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + + +impl system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +parameter_types! { + pub const MaxOwnedDocs: u32 = 100; + pub const MaxSharedToDocs: u32 = 100; + pub const MaxSharedFromDocs: u32 = 100; + pub const DocNameMinLen: u32 = 4; + pub const DocNameMaxLen: u32 = 30; + pub const DocDescMinLen: u32 = 5; + pub const DocDescMaxLen: u32 = 100; +} + + +impl pallet_confidential_docs::Config for Test { + type Event = Event; + type RemoveOrigin = EnsureRoot; + type MaxOwnedDocs = MaxOwnedDocs; + type MaxSharedToDocs = MaxSharedToDocs; + type MaxSharedFromDocs = MaxSharedFromDocs; + type DocNameMinLen = DocNameMinLen; + type DocNameMaxLen = DocNameMaxLen; + type DocDescMinLen = DocDescMinLen; + type DocDescMaxLen = DocDescMaxLen; + +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext() -> sp_io::TestExternalities { + system::GenesisConfig::default().build_storage::().unwrap().into() +} diff --git a/pallets/confidential-docs/src/tests.rs b/pallets/confidential-docs/src/tests.rs new file mode 100644 index 00000000..340ea342 --- /dev/null +++ b/pallets/confidential-docs/src/tests.rs @@ -0,0 +1,490 @@ +use crate::{mock::*, types::*, Error}; +use codec::Encode; +use frame_support::{assert_noop, assert_ok, sp_io::hashing::blake2_256}; +use frame_system as system; + +fn generate_user_id(id: &str) -> UserId { + format!("user id: {}", id).using_encoded(blake2_256) +} + +fn generate_public_key(id: &str) -> PublicKey { + format!("public key: {}", id).using_encoded(blake2_256) +} + +fn generate_cid(id: &str) -> CID { + format!("cid: {}", id).encode().try_into().unwrap() +} + +fn generate_doc_name(id: &str) -> DocName { + format!("doc name:{}", id).encode().try_into().unwrap() +} + +fn generate_doc_desc(id: &str) -> DocDesc { + format!("doc desc:{}", id).encode().try_into().unwrap() +} + +fn generate_owned_doc( + id: &str, + owner: ::AccountId, +) -> OwnedDoc { + OwnedDoc { + cid: generate_cid(id), + name: generate_doc_name(id), + description: generate_doc_desc(id), + owner, + } +} + +fn generate_shared_doc( + id: &str, + from: ::AccountId, + to: ::AccountId, +) -> SharedDoc { + SharedDoc { + cid: generate_cid(id), + name: generate_doc_name(id), + description: generate_doc_desc(id), + from, + to, + } +} + +fn setup_vault(who: ::AccountId) { + let id = &who.to_string(); + assert_ok!(ConfidentialDocs::set_vault( + Origin::signed(who), + generate_user_id(id), + generate_public_key(id), + generate_cid(id) + )); +} + +fn setup_owned_doc(id: &str, owner: ::AccountId) -> OwnedDoc { + let doc = generate_owned_doc(id, owner); + assert_ok!(ConfidentialDocs::set_owned_document(Origin::signed(owner), doc.clone())); + assert_owned_doc(&doc); + doc +} + +fn setup_shared_doc(id: &str, from: ::AccountId, to: ::AccountId) -> SharedDoc { + let doc = generate_shared_doc(id, from, to); + assert_ok!(ConfidentialDocs::share_document(Origin::signed(from), doc.clone())); + assert_shared_doc(&doc); + doc +} + +fn assert_owned_doc(doc: &OwnedDoc){ + assert_eq!(ConfidentialDocs::owned_docs(&doc.cid), Some(doc.clone())); + let owned_docs = ConfidentialDocs::owned_docs_by_owner(doc.owner); + assert_eq!(owned_docs.contains(&doc.cid), true); +} + +fn assert_owned_doc_not_exists(cid: &CID, owner: ::AccountId){ + assert_eq!(ConfidentialDocs::owned_docs(cid), None); + let owned_docs = ConfidentialDocs::owned_docs_by_owner(owner); + assert_eq!(owned_docs.contains(cid), false); +} + + +fn assert_shared_doc(doc: &SharedDoc){ + let SharedDoc { + from, + to, + .. + } = doc; + assert_eq!(ConfidentialDocs::shared_docs(&doc.cid), Some(doc.clone())); + assert_eq!(ConfidentialDocs::shared_docs_by_to(to).contains(&doc.cid), true); + assert_eq!(ConfidentialDocs::shared_docs_by_from(from).contains(&doc.cid), true); +} + +fn assert_shared_doc_not_exists(doc: &SharedDoc){ + let SharedDoc { + from, + to, + .. + } = doc; + assert_eq!(ConfidentialDocs::shared_docs(&doc.cid), None); + assert_eq!(ConfidentialDocs::shared_docs_by_to(to).contains(&doc.cid), false); + assert_eq!(ConfidentialDocs::shared_docs_by_from(from).contains(&doc.cid), false); +} + +#[test] +fn set_vault_works() { + new_test_ext().execute_with(|| { + let owner = 1; + let user_id = generate_user_id("1"); + let public_key = generate_public_key("1"); + let cid = generate_cid("1"); + assert_ok!(ConfidentialDocs::set_vault(Origin::signed(owner), user_id, public_key, cid.clone())); + // Read pallet storage and assert an expected result. + let vault = Vault { cid, owner }; + assert_eq!(ConfidentialDocs::vaults(user_id), Some(vault)); + assert_eq!(ConfidentialDocs::public_keys(owner), Some(public_key)); + + let public_key = generate_public_key("2"); + let cid = generate_cid("2"); + assert_ok!(ConfidentialDocs::set_vault(Origin::signed(owner), user_id, public_key, cid.clone())); + // Read pallet storage and assert an expected result. + let vault = Vault { cid, owner }; + assert_eq!(ConfidentialDocs::vaults(user_id), Some(vault)); + assert_eq!(ConfidentialDocs::public_keys(owner), Some(public_key)); + // assert_eq!(last_event(), Event::VaultStored(user_id, public_key, )) + }); +} + +#[test] +fn set_vault_should_fail_for_empty_cid() { + new_test_ext().execute_with(|| { + let user_id = generate_user_id("1"); + let public_key = generate_public_key("1"); + let cid: CID = Vec::new().try_into().unwrap(); + assert_noop!( + ConfidentialDocs::set_vault(Origin::signed(1), user_id, public_key, cid), + Error::::CIDNoneValue + ); + }); +} + +#[test] +fn set_vault_should_fail_for_origin_not_owner_of_user_id() { + new_test_ext().execute_with(|| { + let user_id = generate_user_id("1"); + let public_key = generate_public_key("1"); + let cid = generate_cid("1"); + assert_ok!(ConfidentialDocs::set_vault(Origin::signed(1), user_id, public_key, cid.clone())); + assert_noop!( + ConfidentialDocs::set_vault(Origin::signed(1), generate_user_id("2"), public_key, cid), + Error::::NotOwnerOfUserId + ); + }); +} + +#[test] +fn set_vault_should_fail_for_origin_not_owner_of_vault() { + new_test_ext().execute_with(|| { + let user_id = generate_user_id("1"); + let public_key = generate_public_key("1"); + let cid = generate_cid("1"); + assert_ok!(ConfidentialDocs::set_vault(Origin::signed(1), user_id, public_key, cid.clone())); + assert_noop!( + ConfidentialDocs::set_vault(Origin::signed(2), user_id, public_key, cid), + Error::::NotOwnerOfVault + ); + }); +} + + +#[test] +fn set_owned_document_works() { + new_test_ext().execute_with(|| { + let owner = 1; + setup_vault(owner); + let mut doc1 = generate_owned_doc("1", owner); + assert_ok!(ConfidentialDocs::set_owned_document(Origin::signed(owner), doc1.clone())); + assert_eq!(ConfidentialDocs::owned_docs(&doc1.cid), Some(doc1.clone())); + let owned_docs = ConfidentialDocs::owned_docs_by_owner(owner); + let expected_cid_vec = vec!(doc1.cid.clone()); + assert_eq!(owned_docs.into_inner(), expected_cid_vec); + doc1.name = generate_doc_name("2"); + doc1.description = generate_doc_desc("2"); + assert_ok!(ConfidentialDocs::set_owned_document(Origin::signed(owner), doc1.clone())); + assert_eq!(ConfidentialDocs::owned_docs(&doc1.cid), Some(doc1.clone())); + let owned_docs = ConfidentialDocs::owned_docs_by_owner(owner); + assert_eq!(owned_docs.into_inner(), expected_cid_vec); + }); +} + +#[test] +fn set_owned_document_should_fail_for_updating_non_owned_doc() { + new_test_ext().execute_with(|| { + let owner = 1; + setup_vault(owner); + let mut doc1 = generate_owned_doc("1", owner); + assert_ok!(ConfidentialDocs::set_owned_document(Origin::signed(owner), doc1.clone())); + doc1.name = generate_doc_name("2"); + let owner = 2; + setup_vault(owner); + assert_noop!(ConfidentialDocs::set_owned_document(Origin::signed(owner), doc1.clone()), Error::::NotDocOwner); + }); +} + +#[test] +fn set_owned_document_should_fail_for_empty_cid() { + new_test_ext().execute_with(|| { + let mut doc = generate_owned_doc("1", 1); + doc.cid = Vec::new().try_into().unwrap(); + assert_noop!(ConfidentialDocs::set_owned_document(Origin::signed(1), doc.clone()), Error::::CIDNoneValue); + }); +} + +#[test] +fn set_owned_document_should_fail_for_name_too_short() { + new_test_ext().execute_with(|| { + let mut doc = generate_owned_doc("1", 1); + doc.name = "as".encode().try_into().unwrap(); + assert_noop!(ConfidentialDocs::set_owned_document(Origin::signed(1), doc.clone()), Error::::DocNameTooShort); + }); +} + +#[test] +fn set_owned_document_should_fail_for_description_too_short() { + new_test_ext().execute_with(|| { + let mut doc = generate_owned_doc("1", 1); + doc.description = "des".encode().try_into().unwrap(); + assert_noop!(ConfidentialDocs::set_owned_document(Origin::signed(1), doc.clone()), Error::::DocDescTooShort); + }); +} + +#[test] +fn set_owned_document_should_fail_for_owner_with_no_public_key() { + new_test_ext().execute_with(|| { + let owner = 1; + let doc = generate_owned_doc("1", owner); + assert_noop!(ConfidentialDocs::set_owned_document(Origin::signed(owner), doc.clone()), Error::::AccountHasNoPublicKey); + }); +} + +#[test] +fn remove_owned_document_works() { + new_test_ext().execute_with(|| { + let owner = 1; + setup_vault(owner); + let doc1 = setup_owned_doc("1", owner); + let doc2 = setup_owned_doc("2", owner); + assert_ok!(ConfidentialDocs::remove_owned_document(Origin::signed(owner), doc1.cid.clone())); + assert_owned_doc_not_exists(&doc1.cid, owner); + assert_owned_doc(&doc2); + assert_ok!(ConfidentialDocs::remove_owned_document(Origin::signed(owner), doc2.cid.clone())); + assert_owned_doc_not_exists(&doc2.cid, owner); + }); +} + +#[test] +fn remove_owned_document_should_fail_for_non_existant_document() { + new_test_ext().execute_with(|| { + let owner = 1; + let doc1 = generate_owned_doc("1", owner); + assert_noop!(ConfidentialDocs::remove_owned_document(Origin::signed(owner), doc1.cid.clone()), Error::::DocNotFound); + }); +} + +#[test] +fn remove_owned_document_should_fail_for_not_document_owner() { + new_test_ext().execute_with(|| { + let owner = 1; + setup_vault(owner); + let doc1 = setup_owned_doc("1", owner); + let not_owner = 2; + assert_noop!(ConfidentialDocs::remove_owned_document(Origin::signed(not_owner), doc1.cid.clone()), Error::::NotDocOwner); + }); +} + +#[test] +fn share_document_works() { + new_test_ext().execute_with(|| { + let to = 1; + let from = 2; + setup_vault(to); + setup_vault(from); + let shared_doc1 = generate_shared_doc("1", from, to); + assert_ok!(ConfidentialDocs::share_document(Origin::signed(from), shared_doc1.clone())); + // Read pallet storage and assert an expected result. + assert_eq!(ConfidentialDocs::shared_docs(&shared_doc1.cid), Some(shared_doc1.clone())); + let shared_docs_to = ConfidentialDocs::shared_docs_by_to(to); + let mut expected_cid_to_vec = vec!(shared_doc1.cid.clone()); + assert_eq!(shared_docs_to.into_inner(), expected_cid_to_vec); + let expected_cid_from_vec = vec!(shared_doc1.cid.clone()); + let shared_docs_from = ConfidentialDocs::shared_docs_by_from(from); + assert_eq!(shared_docs_from.into_inner(), expected_cid_from_vec); + + let from = 3; + setup_vault(3); + let shared_doc2 = generate_shared_doc("2", from, to); + assert_ok!(ConfidentialDocs::share_document(Origin::signed(from), shared_doc2.clone())); + assert_eq!(ConfidentialDocs::shared_docs(&shared_doc2.cid), Some(shared_doc2.clone())); + let shared_docs_to = ConfidentialDocs::shared_docs_by_to(to); + expected_cid_to_vec.push(shared_doc2.cid.clone()); + assert_eq!(shared_docs_to.into_inner(), expected_cid_to_vec); + let expected_cid_from_vec = vec!(shared_doc2.cid.clone()); + let shared_docs_from = ConfidentialDocs::shared_docs_by_from(from); + assert_eq!(shared_docs_from.into_inner(), expected_cid_from_vec); + }); +} + +#[test] +fn share_document_should_fail_for_empty_cid() { + new_test_ext().execute_with(|| { + let mut shared_doc = generate_shared_doc("1", 1, 2); + shared_doc.cid = Vec::new().try_into().unwrap(); + assert_noop!( + ConfidentialDocs::share_document(Origin::signed(1), shared_doc.clone()), + Error::::CIDNoneValue + ); + }); +} + +#[test] +fn share_document_should_fail_for_name_too_short() { + new_test_ext().execute_with(|| { + let mut shared_doc = generate_shared_doc("1", 1, 2); + shared_doc.name = "as".encode().try_into().unwrap(); + assert_noop!( + ConfidentialDocs::share_document(Origin::signed(1), shared_doc.clone()), + Error::::DocNameTooShort + ); + }); +} + +#[test] +fn share_document_should_fail_for_desc_too_short() { + new_test_ext().execute_with(|| { + let mut shared_doc = generate_shared_doc("1", 1, 2); + shared_doc.description = "des".encode().try_into().unwrap(); + assert_noop!( + ConfidentialDocs::share_document(Origin::signed(1), shared_doc.clone()), + Error::::DocDescTooShort + ); + }); +} + +#[test] +fn share_document_should_fail_for_share_to_self() { + new_test_ext().execute_with(|| { + let to = 1; + let from = 1; + setup_vault(to); + let shared_doc = generate_shared_doc("1", from, to); + assert_noop!( + ConfidentialDocs::share_document(Origin::signed(from), shared_doc.clone()), + Error::::DocSharedWithSelf + ); + }); +} + +#[test] +fn share_document_should_fail_for_doc_already_shared() { + new_test_ext().execute_with(|| { + let to = 1; + let from = 2; + setup_vault(to); + setup_vault(from); + let shared_doc = generate_shared_doc("1", from, to); + assert_ok!(ConfidentialDocs::share_document(Origin::signed(from), shared_doc.clone())); + assert_noop!( + ConfidentialDocs::share_document(Origin::signed(from), shared_doc.clone()), + Error::::DocAlreadyShared + ); + }); +} + +#[test] +fn share_document_should_fail_for_from_with_no_public_key() { + new_test_ext().execute_with(|| { + let to = 1; + let from = 2; + setup_vault(to); + let shared_doc = generate_shared_doc("1", from, to); + assert_noop!( + ConfidentialDocs::share_document(Origin::signed(from), shared_doc.clone()), + Error::::AccountHasNoPublicKey + ); + }); +} + +#[test] +fn share_document_should_fail_for_to_with_no_public_key() { + new_test_ext().execute_with(|| { + let to = 1; + let from = 2; + setup_vault(from); + let shared_doc = generate_shared_doc("1", from, to); + assert_noop!( + ConfidentialDocs::share_document(Origin::signed(from), shared_doc.clone()), + Error::::AccountHasNoPublicKey + ); + }); +} +#[test] +fn update_shared_document_metadata_works() { + new_test_ext().execute_with(|| { + let to = 1; + let from = 2; + setup_vault(to); + setup_vault(from); + let mut shared_doc1 = setup_shared_doc("1", from, to); + shared_doc1.name = generate_doc_name("2"); + shared_doc1.description = generate_doc_desc("2"); + assert_ok!(ConfidentialDocs::update_shared_document_metadata(Origin::signed(to), shared_doc1.clone())); + assert_shared_doc(&shared_doc1); + }); +} + +#[test] +fn update_shared_document_metadata_should_fail_for_non_existant_doc() { + new_test_ext().execute_with(|| { + let to = 1; + let from = 2; + let shared_doc1 = generate_shared_doc("1", from, to); + assert_noop!(ConfidentialDocs::update_shared_document_metadata(Origin::signed(to), shared_doc1.clone()), Error::::DocNotFound); + }); +} + +#[test] +fn update_shared_document_metadata_should_fail_for_not_doc_sharee() { + new_test_ext().execute_with(|| { + let to1 = 1; + let to2 = 2; + let from = 3; + setup_vault(to1); + setup_vault(to2); + setup_vault(from); + let shared_doc1 = setup_shared_doc("1", from, to1); + assert_noop!(ConfidentialDocs::update_shared_document_metadata(Origin::signed(to2), shared_doc1.clone()), Error::::NotDocSharee); + assert_noop!(ConfidentialDocs::update_shared_document_metadata(Origin::signed(from), shared_doc1.clone()), Error::::NotDocSharee); + }); +} + +#[test] +fn remove_shared_document_works() { + new_test_ext().execute_with(|| { + let to1 = 1; + let to2 = 2; + let from = 3; + setup_vault(to1); + setup_vault(to2); + setup_vault(from); + let shared_doc1 = setup_shared_doc("1", from, to1); + let shared_doc2 = setup_shared_doc("2", from, to2); + assert_ok!(ConfidentialDocs::remove_shared_document(Origin::signed(to1), shared_doc1.cid.clone())); + assert_shared_doc_not_exists(&shared_doc1); + assert_shared_doc(&shared_doc2); + assert_ok!(ConfidentialDocs::remove_shared_document(Origin::signed(to2), shared_doc2.cid.clone())); + assert_shared_doc_not_exists(&shared_doc2); + }); +} + +#[test] +fn remove_shared_document_should_fail_for_non_existant_doc() { + new_test_ext().execute_with(|| { + let to = 1; + let from = 2; + let shared_doc1 = generate_shared_doc("1", from, to); + assert_noop!(ConfidentialDocs::remove_shared_document(Origin::signed(to), shared_doc1.cid.clone()), Error::::DocNotFound); + }); +} + +#[test] +fn remove_shared_document_should_fail_for_not_doc_sharee() { + new_test_ext().execute_with(|| { + let to1 = 1; + let to2 = 2; + let from = 3; + setup_vault(to1); + setup_vault(to2); + setup_vault(from); + let shared_doc1 = setup_shared_doc("1", from, to1); + assert_noop!(ConfidentialDocs::remove_shared_document(Origin::signed(to2), shared_doc1.cid.clone()), Error::::NotDocSharee); + assert_noop!(ConfidentialDocs::remove_shared_document(Origin::signed(from), shared_doc1.cid.clone()), Error::::NotDocSharee); + }); +} + diff --git a/pallets/confidential-docs/src/types.rs b/pallets/confidential-docs/src/types.rs new file mode 100644 index 00000000..f0de5f34 --- /dev/null +++ b/pallets/confidential-docs/src/types.rs @@ -0,0 +1,58 @@ +//! Defines the types required by the confidential docs pallet +use super::*; +use frame_support::pallet_prelude::*; + +/// Defines the type used by fields that store an IPFS CID +pub type CID = BoundedVec>; +/// Defines the type used by fields that store a public key +pub type PublicKey = [u8;32]; +/// Defines the type used by fields that store a UserId +pub type UserId = [u8;32]; +/// Defines the type used by fields that store a document name +pub type DocName = BoundedVec::DocNameMaxLen>; +/// Defines the type used by fields that store a document description +pub type DocDesc = BoundedVec::DocDescMaxLen>; + +/// User vault, the vault stores the cipher private key used to cipher the user documents. +/// The way the user vault is ciphered depends on the login method used by the user +#[derive(CloneNoBound,Encode, Decode, RuntimeDebugNoBound, Default, TypeInfo, MaxEncodedLen, PartialEq)] +#[scale_info(skip_type_params(T))] +#[codec(mel_bound())] +pub struct Vault{ + /// IPFS CID where the vault data is stored + pub cid: CID, + /// Owner of the vault + pub owner: T::AccountId, +} + +/// Owned confidential document +#[derive(CloneNoBound,Encode, Decode, RuntimeDebugNoBound, Default, TypeInfo, MaxEncodedLen, PartialEq)] +#[scale_info(skip_type_params(T))] +#[codec(mel_bound())] +pub struct OwnedDoc{ + /// IPFS CID where the document data is stored + pub cid: CID, + /// User provided name for the document + pub name: DocName, + /// User provided description for the document + pub description: DocDesc, + /// Owner of the document + pub owner: T::AccountId, +} + +/// Shared confidential document +#[derive(CloneNoBound,Encode, Decode, RuntimeDebugNoBound, Default, TypeInfo, MaxEncodedLen, PartialEq)] +#[scale_info(skip_type_params(T))] +#[codec(mel_bound())] +pub struct SharedDoc{ + /// IPFS CID where the document data is stored + pub cid: CID, + /// User provided name for the document + pub name: DocName, + /// User provided description for the document + pub description: DocDesc, + /// User that shared the document + pub from: T::AccountId, + /// User to which the document was shared + pub to: T::AccountId, +} \ No newline at end of file diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index dbb7231d..50282264 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -65,6 +65,7 @@ pallet-template = { version = "4.0.0-dev", default-features = false, path = "../ pallet-fruniques = { version = "0.1.0-dev", default-features = false, path = "../pallets/fruniques" } pallet-nbv-storage = { version = "4.0.0-dev", default-features = false, path = "../pallets/nbv-storage" } pallet-gated-marketplace = { version = "4.0.0-dev", default-features = false, path = "../pallets/gated-marketplace" } +pallet-confidential-docs = { version = "4.0.0-dev", default-features = false, path = "../pallets/confidential-docs" } [build-dependencies] substrate-wasm-builder = { version = "5.0.0-dev", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.23" } @@ -101,6 +102,7 @@ std = [ "pallet-node-authorization/std", "pallet-nbv-storage/std", "pallet-gated-marketplace/std", + "pallet-confidential-docs/std", "sp-api/std", "sp-block-builder/std", "sp-consensus-aura/std", diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 11863f10..4e811fb0 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -596,6 +596,31 @@ impl pallet_nbv_storage::Config for Runtime { type MaxProposalsPerVault = MaxProposalsPerVault; } +parameter_types! { + pub const MaxOwnedDocs: u32 = 100; + pub const MaxSharedFromDocs: u32 = 100; + pub const MaxSharedToDocs: u32 = 100; + pub const DocNameMinLen: u32 = 3; + pub const DocNameMaxLen: u32 = 50; + pub const DocDescMinLen: u32 = 5; + pub const DocDescMaxLen: u32 = 100; +} + +impl pallet_confidential_docs::Config for Runtime { + type Event = Event; + type RemoveOrigin = EnsureOneOf< + EnsureRoot, + pallet_collective::EnsureProportionAtLeast, + >; + type MaxOwnedDocs = MaxOwnedDocs; + type MaxSharedFromDocs = MaxSharedFromDocs; + type MaxSharedToDocs = MaxSharedToDocs; + type DocNameMinLen = DocNameMinLen; + type DocNameMaxLen = DocNameMaxLen; + type DocDescMinLen = DocDescMinLen; + type DocDescMaxLen = DocDescMaxLen; +} + parameter_types! { pub const MaxRecursions: u32 = 10; @@ -688,6 +713,7 @@ construct_runtime!( GatedMarketplace: pallet_gated_marketplace, Assets: pallet_assets, NBVStorage: pallet_nbv_storage, + ConfidentialDocs: pallet_confidential_docs, } );