Skip to content

Access control

A component app runs on its group’s leader, and its writes are the log’s: no other node replays them. So a rule about who may write is a check in the method, before anything is written. Two layers enforce it:

  1. The node, from the method’s attributes in the ABI, before the app runs.
  2. The app, with guards and env::caller(), inside the method.
Attribute The node refuses
none Callers who are not signed in (401).
#[app::public] Nothing: anyone may call it on a public app, optionally rate-limited (rate = "10/min").
#[app::confirm] A call without a fresh passkey assertion.
#[app::allow(system)] Only the node itself calls it (timers imply it).
#[app::allow(app = "<id>")] Cross-app calls from any other app.
#[app::allow(action)] Anyone but the app’s own actions.

A method with no attribute still admits any signed-in account the node lets reach the app. Membership of the app’s org is the app’s to check.

pub fn set_user_simple(&mut self, value: String) -> app::Result<()> {
guards::require_member()?;
self.user_simple.set(&value)?;
Ok(())
}

guards::require_member() and guards::require_admin() return the caller’s account, or fail with LoginRequired (not an account: public, system or another app) or Forbidden (below the role). For anything else, match on the caller yourself, as apps/permissions-showcase does:

use calimero_sdk::env::Caller;
fn caller() -> app::Result<String> {
match env::caller() {
Caller::Account(a) | Caller::ApiKey { account: a, .. } => Ok(a),
_ => Err(AppError::login_required()),
}
}

env::org_role(&account) gives Guest, Member, Admin or Owner (ordered); env::in_team(&account, team) checks a team.

Each rule below is a key layout plus a check, all from the component apps in apps/ of core.

Rule Shape
Only the author edits it Key the row by (author, id) with the author taken from the caller, never from an argument. Nobody can name another’s row.
Write once Key by (…, author) and refuse when the row exists (AlreadyWritten); offer no edit or remove.
Moderated Never edited; removal checks the caller against a moderators table the owner seeded in init.
Each account’s own value PerUser<T>: set/get/remove act on the caller’s account; get_for(account) reads another’s.
Content-addressed, never changed Frozen<T>: insert returns the hash and whether it was new; equal content stores nothing.
Fixed at creation Write it in #[app::init] and expose no method that writes it.
/// Sets `key` in the caller's own nested map.
pub fn set_user_nested(&mut self, key: String, value: String) -> app::Result<()> {
let account = guards::require_member()?;
self.user_nested.insert(&(account.clone(), key.clone()), &value)?;
Ok(())
}

The SDK’s harness runs the real component with an org of your choosing, so a refusal is a test away (Testing):

let mut t = TestApp::new(component, Org::new().admins(["ada"]).members(["bob"]).guests(["eve"]), T0);
assert!(t.as_("eve").call("setUserSimple", json!({ "value": "x" })).is_err());