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:
- The node, from the method’s attributes in the ABI, before the app runs.
- The app, with
guardsandenv::caller(), inside the method.
What the node enforces
Section titled “What the node enforces”| 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.
Guards: the org role
Section titled “Guards: the org role”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.
Write rules as data shapes
Section titled “Write rules as data shapes”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(())}Test it
Section titled “Test it”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());