​Canister guard in Rust on the IC

Photo by Illumination Marketing on Unsplash

I recently discovered ​it was possible to specify a guard function to be executed before update and query functions of canister smart contracts written in Rust on the Internet Computer.
​You might tell me that the following post is just a rip off of the Crate documentation but, as I only figured out this was possible while having a look at the transaction notifier repo of OpenChat, I thought it was worth a post 😄.
​​Want to start building decentralized apps without learning a new programming language? Check out Juno, the open-source Blockchain-as-a-Service platform that makes it faster and easier than ever before to build dapps with frontend code only! ⚡️🚀🤯

​Original approach

​I began my journey with Rust when I migrated my existing Motoko code - i.e. when I upgraded the existing smart contracts of Papyrs.
As these canisters were dedicated to user data, I had to migrate functions that required access control too.
​For this purpose, I implemented comparison of principals - i.e. I match the callers of functions against users that are saved in the state. If they are equals, methods can be executed, if not, I throw errors and reject the call.
​Not knowing how to write guards, I basically duplicated ​if all around the place in every calls that needed to be protected.
use candid::Principal; use ic_cdk::{caller, trap}; use ic_cdk_macros::{query, init}; use std::cell::RefCell; #[derive(Default)] pub struct State { pub user: Option<Principal>, } // This canister cannot be created without user #[init] fn init(user: Principal) { STATE.with(|state| { *state.borrow_mut() = State { user: Some(user) }; }); } // Mutable global state. // See Roman Kashitsyn's post for more info: // https://mmapped.blog/posts/01-effective-rust-canisters.html thread_local! { static STATE: RefCell<State> = RefCell::default(); } #[query] fn greet(name: String) -> String { let user: Principal = STATE.with(|state| state.borrow().user).unwrap(); // 🖖 Here I check if the caller matches the user who owns the canister if user != caller() { trap("User does not have the permission to say hello."); } format!("Hello, {}!", name) }
​While it works as I expected, you can easily imagine that duplicating the same code - particularly as the number of functions grows - slowly bloated the readability.
That was before I fortunately discovered the guard feature 💪.

Guard

Out of the box, guards are functions that can be executed before an update or query function. When these returns an error, the related method is not proceed.
So I changed my code to take advantage of it and avoid duplication.
I created a new ​guards.rs module to execute the exact same pattern matching as the one I implemented in above code snippet.
use candid::Principal; use ic_cdk::caller; use crate::STATE; pub fn caller_is_user() -> Result<(), String> { let caller = caller(); let user: Principal = STATE.with(|state| state.borrow().user).unwrap(); if caller == user { Ok(()) } else { Err("Caller is not the user of the canister.".to_string()) } }
​Once set, I then "just" replaced my existing pseudo tests with the declaration of the guard.
// ️🖖1️⃣ declare the new module mod guards; use candid::Principal; use ic_cdk::{caller, trap}; use ic_cdk_macros::{query, init}; use std::cell::RefCell; // 🖖2️⃣ the function needs to be imported use crate::guards::caller_is_user; #[derive(Default)] pub struct State { pub user: Option<Principal>, } // This canister cannot be created without user #[init] fn init(user: Principal) { STATE.with(|state| { *state.borrow_mut() = State { user: Some(user) }; }); } // Mutable global state. // See Roman Kashitsyn's post for more info: // https://mmapped.blog/posts/01-effective-rust-canisters.html thread_local! { static STATE: RefCell<State> = RefCell::default(); } // 🖖3️⃣ set the guard by its function's name #[query(guard = "caller_is_user")] fn greet(name: String) -> String { format!("Hello, {}!", name) }
​And, that is basically it already 🥳.
​The recipe:
  1. ​Create a guard function that returns an error if conditions are not met
  2. Import the module
  3. Annotate the functions that need to be protected​
  4. Having fun 😁

​Summary

​Not my longest post ever wrote but, I hope it will be useful for someone someday as it was for me to discover this small tricks.
​To infinity and beyond
David​

For more adventures, follow me on Twitter 🖖