Rust guard access arguments

In a Rust canister, is it possible to access the function’s arguments in a guard?

pub fn caller_is_admin() -> Result<(), String> {
    let caller = caller();
    let hello: Hello = STATE.with(|state| state.borrow().heap.hello.clone());
    
   // Here accessing the args ???

   if caller == hello.key && args.something == hello.something {
      Ok(())
   } else {
        Err("Caller is not allowed.".to_string())
    }
}


#[query(guard = "caller_is_admin")]
fn get_something(args: MyArgs) {
  ...
}
1 Like

It looks to me like this guard is not called with any arguments so there’s no easy way to do this: https://github.com/dfinity/cdk-rs/blob/0f674bd5adfd5323ee3d1777acd5a850fa3cbd01/src/ic-cdk-macros/src/export.rs#L180-L197

I think it should be possible to update the guard to receive the same arguments as the function its guarding: https://github.com/dfinity/cdk-rs/blob/0f674bd5adfd5323ee3d1777acd5a850fa3cbd01/src/ic-cdk-macros/src/export.rs#L153-L157, But then it would be difficult to use the same guard across multiple functions due to different argument types.

For the case that you’re encountering, I just call an assertion function manually at the start of my endpoint’s function:

#[query]
fn get_something(args: MyArgs) {
  assert_caller_is_admin(args.something)?;
  ...
}

There’s nothing really special about the “guard” function, it’s just a function being called before your primary endpoint’s function. So there’s no meaningful difference between those two approaches except how you invoke them, but calling your function manually gives you more flexibility.

Thanks Nathan!

Indeed in my real use case I wrote an assertion function instead of using a guard but, I was curious about it anways. As there is a caller() function, I was wondering if maybe there is another function that deserialize the args and can be use anywhere, that way it would have been possible to access the args in a guard. I guess it doesn’t exist then and it is not possible.

Not an issue, was more a curiosity question.

arg_data in ic_cdk::api::call - Rust.

1 Like

Actually there is. A guard function is the only way that the ‘reject’ functionality is accessed. A canister function must either reply, reject, or trap. The macros unconditionally reply at the end of the function, because forcing the user to call reply would be annoying, but this then prevents the user from calling reject, because the auto-inserted reply will then cause a trap. A guard function is the only way to reject without trapping (using the CDK macros).

And yes, to get the arguments, you can use arg_data. It’s the same function that is used to produce the arguments for the canister method itself - the real symbol exported to wasm is () -> ().

3 Likes

Thanks for the added info Adam!

A guard function is the only way that the ‘reject’ functionality is accessed

I wasn’t aware of that aspect of the guard function, that’s good to know, thanks! Is there any advantage to rejecting over trapping?

And yes, to get the arguments, you can use arg_data

Calling arg_data would deserialize the arguments again though, right? Is it worth paying the extra cycles for deserialization to be able to reject instead of trapping?

1 Like

Thanks @levi and @AdamS :+1:

That’s a good point @NathanosDev, thanks. In the particular feature I’m building, beside cycles, speed matters so from a design perspective might be good to not add an extra deserialize.

Using the manual_reply = true parameter in the update or query macros turns off the automatic reply, letting either a manual reply or a manual reject. So you can reject without trapping using the cdk macros. Be ware though that a reply or a reject must be called before the method returns.

For those curious, reject and trap are different. For a reject the canister still commits the state changes of the current message. For a trap the system rolls back the state changes of the current message.

2 Likes

Thanks! So when we’re talking about guard functions we can probably safely assume that they’re not going to alter state, so rejecting and trapping would essentially be the same if there’s no other differences.

Sure, glad to spread the good word.

I think that is an unsafe assumption, what if a canister puts a lock on the user in the guard function, or counts user calls for rate limiting or similar.

4 Likes