How to do validation on a custom function of a module?

In a definition of Entity, is there a way to check what function in a module is being called and do validation on it?

In a way similar to this:

#[derive(Serialize, Deserialize, Debug, Clone, DefaultJson)]
pub struct Article {
   /*[......]*/
}

pub fn approve_article(addr: Address) -> ZomeApiResult<()> {
  /*[........]*/
}

pub fn definition() -> ValidatingEntryType {
    entry!(
        name: "article",
        description: "",
        sharing: Sharing::Public,
        validation_package: || {
          hdk::ValidationPackageDefinition::Entry
        },

        validation: |validation_data: hdk::EntryValidationData<Order>| {
          match validation_data {


              //good, but this will work for 'Article::new()' only

              EntryValidationData::Create {entry, validation_data} => {
                let chain_header = &validation_data.package.chain_header;
                if validation_data.sources().contains(&HashString::from("MY_MAIN_AGENT_ADDRESS")) {
                  Ok(())
                } else {

                  [................]


        //Q: how to do validation for 'approve_article(....)' as well???

I want to be able to permit/reject certain agents from calling approve_article function.

Probably the best way to think about this is not so much that certain agents are permitted/rejected re: calling approve_article() but that the side effects of approve_article() are permitted/rejected. That is, any entry that gets written as a result of calling approve_article() passes or fails validation based on the agent ID. This is because, in a distributed system where we can’t guarantee that our peers are running the right code (even if they have proof that they are), all we can do is validate the data they produce. This is true for blockchain as well.

Validation should depend entirely on the contents of the entry itself + the contents of the validation package. It should yield the same result regardless of whether it’s being processed by the author, a counterparty, or a third-party validator. Therefore:

  • We can’t use globals that vary by location or time such as hdk::AGENT_ADDRESS or the local clock.
  • We can’t check the current state of the DHT, because it’s always changing. That means that any proof that the author has the right to publish an entry should be part of the entry — usually as a signature of someone who has the power to grant that right.

I realise this is a complex topic, and we’re working on some introductory literature about how to implement this… but it’s not going to be ready for a month or so :confused: But it’s very similar to how SSL certificates work. Here’s a table that shows the parallels:

SSL certificate infrastructure Certificates in Holochain app
Certificates are documents, signed by an authority, that outlining the authority being granted Same
CA root authority, trusted by browser Root admin user, trusted explicitly by DNA
Enforced by browser Enforced by end-user and validators

This isn’t something supported by the HDK, but it should be fairly straightforward to implement this with app-level primitives:

  1. The DNA specifies the root authority in the properties section of app.json — this would probably look like a public key that all users should recognise as the admin user’s public key.

  2. The root authority authorises other people to do things by specifying their privileges in a certificate. Here’s a simple example of what that might look like:

    pub enum Privilege {
        AuthorArticles,
        ApproveArticles,
        DeleteArticles,
        // ... etc
    }
    
    pub struct PrivilegeCertificateBody {
        /// What's the privilege being granted?
        privilege: Privilege,
        /// The 'subject' is the person who's being given a privilege. This
        /// looks like their agent ID.
        subject: Address,
        /// Just as with SSL certs, privileges should have an expiry in case
        /// you want to revoke that privilege later. A subject should always
        /// produce a certificate that's currently valid, and an authority
        /// should always create a new certificate for the subject when they
        /// ask, unless the authority has reason to revoke that privilege.
        expires: Option<DateTime>,
    }
    
    pub struct PrivilegeCertificate {
        /// This is the thing being signed.
        body: PrivilegeCertificateBody,
        /// This is the authority's signature on the body, along with their
        /// public key, which proves that they've granted this authority.
        provenance: holochain_core_types::signature::Provenance,
    }
    
  3. Any action that requires privileges should include a reference to the certificate that grants the privilege. In the future, you’ll be able to do that by specifying the hash of the certificate:

    pub struct ArticleApproval {
        privilege_certificate: Address,
        // ... fields of approval struct
    }
    

    but for now I believe you’d have to embed the whole certificate in the struct:

    pub struct ArticleApproval {
        privilege_certificate: PrivilegeCertificate,
        // ... fields of approval struct
    }
    
  4. The validation rules for an action (such as article approval) should check the validity of the certificate:

    1. Its public key should be an authority recognised by the app.
    2. Its signature should actually be valid for the public key and PrivilegeCertificateBody (only strings can be signed, so the body should be JSON-ified before signing).
    3. Its subject and granted privilege should match the action that the agent is trying to take.

    In the future, when you’re able to reference the certificate by hash, Holochain will automatically perform validation on that certificate before performing validation on the article approval action. Then, for validating the article approval entry, you’ll only have to check condition 3 in the list above (subject and granted privilege match the action being performed).

3 Likes

That’s too complex for my needs at the moment.

In validation: |validation_data: hdk::EntryValidationData<Order>| { ... } is there a way to know what function is in charge for calling this? Say, it might be my custom function “my_custom_update_entity(…)” or “set_entity_status(…)”, or “delete_entity(…)” etc

In other words, when an agent is calling “my_custom_update(…)” which is supposed to update an Entity, I want to be able to retrieve its name – “my_custom_update” – in
“validation”. And then allow or reject the actual update operation.

Or: in validation: |validation_data: hdk::EntryValidationData<Order>| { ... } how can I know what fields of an Entity are being updated?

Validation is a call back when you attempt to change an entry, it does not track which function updated it nor does it track the changes to the entry.
For what you have described I’d probably try to add that feature into the “my_custom_update” function not the validation.

In that case, How can I call

if validation_data.sources().contains(&HashString::from("MY_MAIN_AGENT_ADDRESS")) { ... }

from within a function?

I need the key/ID of an agent that’s calling a function

@alx may I ask why the validation function needs to know what function created the entry it’s trying to validate? You could put a field into the entry like called_from: String, but that’s easy for any malicious agent to fake, so you can’t rely on it being true. And to reiterate about context in a validation function:

A validation function should always be stateless. We can’t be sure where a validation function is going to be called, and that’s by design — the result shouldn’t depend on who’s running the validation. It should always be either true or false based on the entry and the validation package alone. That’s why the DHT itself should enforce rights to edit an entry or specific fields in an entry.

Thoughts on updating certain fields: regardless of how you authorise a certain agent to edit such-and-such a field, what you could do is compare the updated entry to the original – if any field has changed in the updated entry, and the agent isn’t allowed to update that field, then validation fails. The original can be found in EntryValidationData::Modify { old_entry }
. Then the problem is reformulated from

“agent A may only edit fields X and Y of entry type E”

to

“when agent A creates an updated entry for entry type E, they must leave all fields unchanged except X and Y”

Regarding how to authorize, it’d help us if you mapped out your use case in more detail — exactly who should be allowed to update an entry (only the original author, only the official ‘admin’, a mutable group of privileged people, etc). Could you give us a clearer picture of your authorisation needs?

1 Like

this

For now - in the simplest way possible. Namely, by hardcoding their IDs in the source code. Will this work ok?

In that case, is this incorrect?

             //[.........]
              EntryValidationData::Create {entry, validation_data} => {
                let chain_header = &validation_data.package.chain_header;
                if validation_data.sources().contains(&HashString::from("MY_MAIN_AGENT_ADDRESS")) {
                  Ok(())
                } else {

That will work. You can also put their Ids in the properties section of the dna.json file. I’m just doing an example of that right now and will post here later.

Other thing to think about: Is there a way you can achieve your goal without having old school ideas like admins and approvers? In an agent centric world you can use membranes (doing an example of this too) and signatures to do things like approve. Imagine a setup where you have the “open” DHT where unapproved articles get published and another DHT that has the “approved” articles. Then you setup a tighter membrane on the “approved” DHT and only allow those agents to bridge between them and “approve” the article frpm the open DHT to the approved DHT.

1 Like

Here’s an example of using the properties in the dna.json file to limit who can access the DNA. I’m using this with Peer Chat for private rooms.

in the dna.json file at the top level.

“properties”: {
“allowed_members”: [“HcScjTnefoi6c79eunbqfFNYEYovwaygbPkWEk95xVPd7vemvoB9Qwbjxf458ii”, “HcSCIUKodbWktcbmwyAaVrYnsbadi6b9sOV9D4ZoE8i333ZtcGw38Jn3U6u63qi”]
},

in lib.rs

#[validate_agent]
pub fn validate_agent(validation_data: EntryValidationData<AgentId>) {
	if let EntryValidationData::Create{entry, ..} = validation_data {
		let properties = hdk::api::property("allowed_members");
		if let Ok(members) = properties {
			let member_list: Vec<String> = serde_json::from_str(&members.to_string()).unwrap();
			hdk::debug(format!("PROPERTIES: {:?}", member_list)).ok();
			let agent = entry as AgentId;
			hdk::debug(format!("AgentId: {:?}", agent)).ok();
			if member_list.contains(&agent.pub_sign_key) {
				Ok(())
			} else {
				Err("This agent is not in the allowed members list".into())
			}
		} else {
			Err("Issue reading members from dna.json".into())
		}
    } else {
		hdk::debug(format!("Cannot update or delete an agent at this time")).ok();
        Err("Cannot update or delete an agent at this time".into())
    }
}

Why? It’s generated automatically and shouldn’t be edited. No? There’s app.json

No

Where is that function called from and by whom? Why is it public?

Its the call back when an Agent joins a DHT. Its in the lib.rs file of your dna.

@alx I said something wrong: I should have said

It should always be either true or false based on the entry, the validation package, or static values alone.

Therefore, "MY_MAIN_AGENT_ADDRESS" is okay because it’s a string literal, and @philipbeadle’s example of embedding allowed addresses in the DNA’s properties is super super versatile.

You can specify a properties object in app.json as you guessed. You can also add to those properties at installation time — check out the conductor admin API (search for admin/dna/install_from_file). It lets you interpolate new values into your properties object, so you can use one base DNA as a template and create new DNAs, each with their own main agent address value. That lets you create multiple little spaces, each of which has its own admin. This could be useful for having individual public blogs where only the blog owner gets to post articles.