You are going to re-implement this private publication happ, one exercise at a time. Every exercise has a series of steps, and in each step there will be some missing feature in the DNA code that you'll have to implement, and then run the tests to confirm that your solution is correct.
These are the instructions for the first step, amenable to all the other steps:
-
Run
nix develop
. This sets up your terminal with all the developer tooling necessary. Run ALL the commands below WITHIN this environment, otherwise they won't work. -
Run
EXERCISE=1 STEP=1 npm test
.
- This is the error message you should see:
14:15:40 [Tryorama - Local Conductor] info: WARNING: running without local db encryption
14:15:40 [Tryorama - Local Conductor] info: WARNING: running without local db encryption
{
type: 'error',
data: {
type: 'ribosome_error',
data: "Attempted to call a zome function that doesn't exist: Zome: private_publication_lobby Fn grant_capability_to_read"
}
}
npm ERR! Lifecycle script `test` failed with error:
npm ERR! Error: command failed
npm ERR! in workspace: [email protected]
npm ERR! at location: /home/guillem/projects/holochain/immersive/private-publication/tests
- Implement the missing function that that step requires (see step 1 in the "Exercise 1: Profiles zome" section of this document).
- Run the tests again, until all tests pass again.
- Move on to the next step, and run the new tests.
- Eg. for the second step, you should run
EXERCISE=1 STEP=2 npm test
.
- When you are done with all the steps in an exercise, move to the next exercise (see step 1 in the "Exercise 2: Comments zome" of this document):
- Eg. for first step of the 2nd exercise, you should run
EXERCISE=2 STEP=1 npm test
.
The private publication app is really similar to the forum one from the first exercise, although with a couple of important architectural differences.
The requirement for this app is that any agent can create their own private publication, for which they have complete read and write permissions. Let us call this agent the "author" of the publication. After creating the publication, its author can grant read permissions for it to any other agent, the "reader". These permissions should be revokable, meaning that at any point in time the author of the publication can remove the read permissions from the reader.
Lastly, the author of a private publication can also grant write permissions to any agent, which we'll call the "editor" of the publication. This permission isn't be revokable.
In this setup, there are two DNAs:
- Lobby: everyone is able to join this DNA. This is where agents negotiate granting of capabilities to read the private publications.
- Private publication: this is a clonable DNA, which means that it's not instantiated at install time. Instead, any author can choose to create its own private publication DNA, and they do so by cloning the private publication DNA and setting their own public key in the properties as the DNA's progenitor (which means they are the administrator for that DNA). Other agents can then be invited to this cloned DNA if the progenitor wants, so that they can also become authors of the publication.
The posts zome for creating and managing the posts is already implemented. In this exercise we are going to create the zome for the lobby DNA, with this functionality:
- Grant permissions to read the private publication to an agent in the lobby.
This is the flow that we've designed for granting read permissions:
You can visualize this diagram in its github page.
sequenceDiagram
participant ReaderLobbyCell
actor Reader
actor Author
participant AuthorLobbyCell
participant AuthorPrivatePublicationCell
Note over AuthorLobbyCell,AuthorPrivatePublicationCell: Author's conductor
Reader->>Author: "the reader asks the author for read permissions"
Author->>+AuthorLobbyCell: grant_capability_to_read(reader_pub_key)
AuthorLobbyCell-->>AuthorLobbyCell: generate_cap_secret()
AuthorLobbyCell-->>AuthorLobbyCell: create_cap_grant(cap_secret, private_publication_dna_hash as tag)
AuthorLobbyCell-->>-Author: cap_secret
Author->>Reader: "the author shares the cap_secret with the reader"
Reader->>+ReaderLobbyCell: store_capability_claim(author_pub_key, cap_secret)
ReaderLobbyCell-->>-ReaderLobbyCell: create_cap_claim(cap_secret)
Note over Author,Reader: From now on the reader is authorized to read the posts
Reader->>+ReaderLobbyCell: read_posts_for_author(author_pub_key)
ReaderLobbyCell->>+AuthorLobbyCell: call_remote(cap_secret, "request_read_private_publication_posts")
AuthorLobbyCell-->>AuthorLobbyCell: call_info()
AuthorLobbyCell-->>AuthorLobbyCell: extract private_publication_dna_hash from call_info.cap_grant
AuthorLobbyCell->>+AuthorPrivatePublicationCell: call(private_publication_dna_hash, "read_all_posts")
AuthorPrivatePublicationCell-->>-AuthorLobbyCell: posts
AuthorLobbyCell-->>-ReaderLobbyCell: posts
ReaderLobbyCell-->>-Reader: posts
Solve the next steps in the private_publication_lobby
coordinator zome, in dnas/lobby/coordinator_zomes/private_publication_lobby/src/lib.rs
.
- Create a
GrantCapabilityToReadInput
struct with two fields:reader
of typeAgentPubKey
andprivate_publication_dna_hash
of typeDnaHash
(which you can use fromhdk::prelude::holo_hash
).
- Annotate this struct with
#[derive(Serialize, Deserialize, Debug)]
. - Create a function
grant_capability_to_read
that receives aGrantCapabilityToReadInput
struct and:- Generates a capability secret with
random_bytes()
. - Converts the
private_publication_dna_hash
to aString
usingDnaHashB64::from(input.private_publication_dna_hash).to_string()
. You can useDnaHashB64
fromhdk::prelude::holo_hash
. - Create an assigned capability grant to call
request_read_private_publication_posts
to the reader, using the stringifiedprivate_publication_dna_hash
as the tag. - Returns the
CapSecret
that was generated.
- Generates a capability secret with
- Create a
StoreCapClaimInput
struct with two fields:author
of typeAgentPubKey
, andcap_secret
of typeCapSecret
.
- Annotate this struct with
#[derive(Serialize, Deserialize, Debug)]
. - Create a function
store_capability_claim
that receives anStoreCapClaimInput
struct and stores a capability claim with the given secret and author.
- Create a function
read_posts_for_author
that receives theAgentPubKey
of the author of the private publication we want to read the posts from, and returns anExternResult<Vec<Record>>
:
- Query the source chain to get the capability claim.
- Call remote to the given author's
request_read_private_publication_posts
and the capability secret found in the capability claim.- Make sure to return the actual error message from the
ZomeCallResponse
if thecall_remote
fails.
- Make sure to return the actual error message from the
- Return the result.
- Create a function
request_read_private_publication_posts
with no inputs that returns anExternResult<Vec<Record>>
. This function should:
- Calls call_info() and extracts the capability grant that was used to call this function.
- Converts the tag from the capability grant to a
DnaHash
, using
let private_publication_dna_hash = DnaHash::from(
DnaHashB64::from_b64_str(zome_call_cap_grant.tag.as_str()).or(Err(wasm_error!(
WasmErrorInner::Guest(String::from("Bad cap_grant tag"))
)))?,
);
- Constructs the private publication cell id with
CellId::new(private_publication_dna_hash, agent_info()?.agent_latest_pubkey)
- Makes a bridge call to the private publication cell, zome name
posts
, and function nameget_all_posts
, converts its result in to aVec<Record>
, and returns that.
We are going to implement these new functionalities:
- As the progenitor of the private publication, I should be able to invite other agents into the private publication DNA.
- As the progenitor of the private publication, only I should be able to assign the editor role to agents in the private publication DNA.
- Only agents with the editor role should be able to create posts in the private publication DNA.
- Only the creator of a post should be able to update it.
Go into crates/membrane_proof/src/lib.rs
.
- Notice the
PrivatePublicationMembraneProof
struct. This is the entry that each author is going to create in the lobby, and is going to share with the editor so that they can join the author's private publication dna. - Notice also that this struct is located in a shared crate, which both the
private_publication_lobby
andprivate_publication
zomes are going to depend upon.
Go into dnas/lobby/coordinator_zomes/private_publication_lobby/src/lib.rs
:
-
Create a
create_membrane_proof_for
zome function that receives aPrivatePublicationMembraneProof
and returns anExternResult<()>
. This function will be executed by the progenitor of the app.- Create a
PrivatePublicationMembraneProof
entry with the private publication DNA hash and the recipient for that membrane.- This entry type is already defined in
dnas/lobby/integrity_zomes/private_publication_lobby/src/lib.rs
. You just need to use that struct, and create en entry of that type in thecreate_membrane_proof_for
zome function.
- This entry type is already defined in
- Create a link from the agent public key of the recipient to the newly created action.
- Create a
-
Create a
get_my_membrane_proof
zome function that doesn't receive any parameters, and returns anExternResult<Option<Record>>
.- Get the links from your public key of type
LinkTypes::AgentToMembraneProof
. - If there is some link, return the record that the target is pointing to.
- Get the links from your public key of type
Go into dnas/private_publication/integrity_zomes/private_publication/src/properties.rs
:
- Add a
Properties
struct, with only aprogenitor
field of typeAgentPubKeyB64
.
- Annotate this struct with
#[derive(Serialize, Deserialize, Debug, SerializedBytes)]
. - Create an extern function
progenitor
that doesn't have any input parameters and that returns anExternResult<AgentPubKey>
with theAgentPubKey
for the progenitor of this DNA.- Get the serialized properties with
dna_info()?.modifiers.properties
. - Transform that serialized properties type into our
Properties
struct.- You can deserialize any type that is annotated with
SerializedBytes
usingtry_from
:Properties::try_from(serialized_bytes).map_err(|err| wasm_error!(err))?
- You can deserialize any type that is annotated with
- Convert the
AgentPubKeyB64
into anAgentPubKey
withAgentPubKey::from()
.
- Get the serialized properties with
Now go into dnas/private_publication/integrity_zomes/private_publication/src/validation.rs
. There you can see boilerplate that allows for the genesis self-check and for different validations for the two kinds of entries present in that DNA.
Go into dnas/private_publication/integrity_zomes/private_publication/src/membrane.rs
.
- Implement the membrane proof check to avoid unwanted agents coming into the DHT:
- If the agent we are validating for is the progenitor, then the membrane proof check is valid.
- If not, deserialize the membrane proof into a
Record
.- You can use
let bytes: SerializedBytes = Arc::try_unwrap(membrane_proof).map_err(|err| wasm_error!(WasmErrorInner::Guest(format!("{:?}", err))))?;
to unwrap theMembraneProof
type intoSerializedBytes
. - You can use
let record = Record::try_from(bytes);
to deserialize aSerializedBytes
into aRecord
.
- You can use
- Check that the author of the action in the record is the progenitor.
- Check that the signature in the
Record
is valid for therecord.action_hashed().content()
of the record. - Deserialize the record's entry into a
PrivatePublicationMembraneProof
.- In general, to extract an entry in the form of its rust struct from a record that contains it, you can use
ENTRY_TYPE::try_from(entry)
. For example, to convert a record from aComment
app struct:let maybe_entry: Option<Entry> = record .entry .into_option(); let entry: Entry = maybe_entry.ok_or(wasm_error!(WasmErrorInner::Guest(String::from("This record doesn't include any entry"))))?; let comment = Comment::try_from(entry)?;
- In general, to extract an entry in the form of its rust struct from a record that contains it, you can use
- Check that the dna hash inside the
PrivatePublicationMembraneProof
is equal to the dna hash of theprivate_publication
DNA. - Check that the agent we are checking the membrane for is the agent that is inside the
PrivatePublicationMembraneProof
.
Go into dnas/private_publication/coordinator_zomes/roles/src/lib.rs
.
- Create an
assign_editor_role
function that takes anAgentPubKey
, and creates aPublicationRole
entry with role equal toString::from("editor")
.
Go into dnas/private_publication/integrity_zomes/private_publication/src/publication_role.rs
.
- Implement the
validate_create_publication_role
, only allowing the progenitor to create thePublicationRole
entry.
Go into dnas/private_publication/integrity_zomes/private_publication/src/post.rs
.
-
Implement the
validate_create_post
function so that only agents for which aPublicationRole
with role "editor" has been created can create posts. -
Implement the
validate_update_post
function so that only the original author of the post can modify their posts.