Skip to content

Authorization and Access

In this guide, you will:

  • Understand the importance of authorization and access control in smart contract security.
  • Learn how to define and apply access rules for components and their methods.
  • Discover how to set permissions for resources, including minting, burning, and metadata access.
  • Delegate a resource’s authorization to a component hook, and keep the ability to repair or retire it.
  • Explore identity management and the different types of callers in the Tari Ootle environment.
  • Follow best practices for securing templates and managing digital assets.

Authorization and access control are critical aspects of smart contract security. The Tari Ootle Template Library provides flexible mechanisms to define who can execute methods on your Components and interact with your Resources.

Access rules determine which identities (Components, users, or other templates) are allowed to perform certain actions. They are typically defined when a Component is created or a Resource is minted.

Access rules are configured with ComponentAccessRules (for component methods) and ResourceAccessRules (for resource actions).

When you create a Component, you can specify access rules for each of its methods. These rules decide which caller may invoke the method.

use tari_template_lib::prelude::*;
#[template]
mod template {
pub struct MyComponent {
// Component state fields
}
pub fn create_with_access_rules(
admin_badge: ResourceAddress,
) -> Component<MyComponent> {
Component::new(MyComponent { /* ... */ })
.with_access_rules(ComponentAccessRules::new()
// Allow anyone to call `public_method`.
.method("public_method", rule!(allow_all))
// Only a caller holding `admin_badge` may call `admin_method`.
.method("admin_method", rule!(resource(admin_badge)))
// Default rule: anyone can call methods not explicitly
// listed. With no default rule, unlisted methods default to
// `deny_all`, so only the owner may call them.
.default(rule!(allow_all))
)
.create()
}
}
  • ComponentAccessRules::new(): Creates a new set of component access rules with a deny_all default.
  • ComponentAccessRules::allow_all(): Creates a set of rules where every method is callable by anyone by default.
  • .method("method_name", AccessRule): Defines an access rule for a specific method.
  • .default(AccessRule): Sets the default rule for any method not explicitly listed (defaults to deny_all).

Ownership is configured separately with .with_owner_rule(OwnerRule::...); the component owner may always call any method. OwnerRule defaults to OwnedBySigner, meaning the transaction signer that created the component owns it.

The rule! macro builds an AccessRule. The base rules are rule!(allow_all) and rule!(deny_all); a restricted rule wraps one or more requirements:

  • rule!(resource(address)) — the caller presents proof of a fungible resource.
  • rule!(non_fungible(address)) / rule!(public_key(pk)) — the caller presents proof of an NFT, or a signature by the given public key.
  • rule!(component(address)) — execution is within the given component (the current frame is that component).
  • rule!(template(address)) — execution is within the given template (the current frame is that template).
  • rule!(caller_component(address)) — the immediate caller is the given component.
  • rule!(direct_caller_template(address)) — the immediate caller is code from the given template: any component instance of it, or any static function of it.
  • rule!(any_caller_component) / rule!(any_caller_template) — there was a caller of that kind, whoever it was.
  • rule!(any_of(...)), rule!(all_of(...)), rule!(m_of_n(n, ...)) — combine requirements.

There are two related but distinct ways a rule can reference a component or template:

  • component(address) / template(address) ask “is execution happening inside this component/template?”. On a method access rule the current (top) frame is the callee, so these describe the callee, not the caller. Use them when a method should only run while that component/template is executing, or when the same rule is evaluated for resource/ownership checks where the current frame is the actor.

  • caller_component(address) / direct_caller_template(address) ask “who called into here?”. The caller is always the direct caller: the frame that was current immediately before this frame was pushed. If A calls B and B calls C, C’s frame sees B, never A.

    direct_caller_template(address) is a package identity, not an instance identity. It matches any component instance of that template, including instances anyone may create through a public constructor, and any static function of that template, which anyone may call from a transaction. It is therefore only as strong as the least careful outgoing call anywhere in that template: a method or static function that forwards a caller-chosen component and method lets anyone act as the template.

Caller identity is issued as a virtual badge, not evaluated once at the door. When the engine pushes a frame it stamps two badges into that frame’s authorization scope — one naming the caller’s component (when the caller has one) and one naming the caller’s template. Two things follow:

  • The badges are checkable wherever any other badge is, and for the whole lifetime of the frame: a component method rule, an owner rule, a resource rule, or a stealth spend condition. A helper called deeper inside the same frame can still re-assert “we are acting for A”.
  • They are re-derived at every call and never inherited. A frame cannot pass on the identity it was called with, and there is no way to capture a caller badge as a Proof and hand it onward.

A method or function invoked directly by a top-level transaction instruction has no caller frame — the caller is a signer — so caller_component / direct_caller_template rules deny it. Use a badge, public key, or other proof requirement to allow signer-driven calls.

Two further forms ask the weaker question “was there a caller at all?”, and take no address:

// Satisfied by any component caller, and by no top-level instruction.
rule!(any_caller_component)
// Satisfied by any frame below the top level, i.e. reached from template code
// rather than directly from a transaction instruction.
rule!(any_caller_template)

You never need to name a badge or a reserved address to write a caller rule. If you do need the badge itself — to combine it with other requirements by hand — NonFungibleAddress::caller_component_badge(addr) and ::direct_caller_template_badge(addr) build it, the same way NonFungibleAddress::from_public_key(pk) does for a signer.

No resource exists at either badge address and none can be created, so these badges cannot be minted, held in a vault, or forged by a template.

Caution: caller_component / direct_caller_template match the immediate caller’s identity, which is only as trustworthy as the code that makes the call. A method that forwards a caller-supplied component/template and method (a “proxy” method) delegates that identity, so anyone who can call the proxy can act as the proxied caller. caller_component(address) also trusts the owner of that component, not its current code: the owner can migrate the component to any template unless it was created with OwnerRule::None.

The same applies to resource auth hooks: the engine invokes the hook with the acting component as the caller, and any resource may bind any hook-shaped method. A caller gate on a hook method is therefore never sufficient on its own; the hook must check AuthHookCaller::resource() against the resources it manages.

Caution, the other way round: calling out hands the callee your identity as a live badge for the whole of its frame, usable at every auth point it reaches — a resource rule, an ownership rule, a spend condition — not only the method it entered through. Weigh that before calling into code you do not control, and gate anything that must not be reachable that way on a proof the callee cannot obtain rather than on the caller badge alone.

A resource auth hook is the one call-out you do not choose: any resource may bind a hook, and anyone may deposit that resource into an account. The engine therefore runs a hook in a sandboxed frame that may read, emit events and update its own component state (when declared &mut self), but cannot write any other substate, act on any vault or resource, or call another component. The acting component’s badge satisfies the hook’s own method rule, and the sandbox is what stops the hook spending it on the acting component’s behalf.

Resources can also have access rules, typically governing who can mint, burn, recall, freeze, or update metadata. Each builder method takes two arguments:

  1. The [AccessRule] that gates the action right now.
  2. An [UpdateRule] that gates who, if anyone, can later change that access rule. The updater is fixed at creation time and cannot itself be changed afterwards — this lets holders trust a Locked rule will stay locked forever.
use tari_template_lib::prelude::*;
#[template]
mod template {
pub struct MyComponent {
resource: ResourceManager,
}
impl MyComponent {
pub fn create_with_restricted_resource(
minter_badge: NonFungibleAddress,
) -> ResourceAddress {
let resource_address = ResourceBuilder::public_fungible()
.with_token_symbol("RTK")
.with_divisibility(10)
.initial_supply(amount!["1000000000000000000000"])
// Only the minter badge can mint, and only the owner may
// change who can mint later.
.mintable(rule!(non_fungible(minter_badge)), OWNER)
// Nobody can burn, and the rule is locked forever.
.burnable(rule!(deny_all), LOCKED)
.build();
MyComponent { resource: resource_address.into() }.create()
}
}
}

Each ResourceBuilder exposes one method per action. All accept (rule, updater):

  • .mintable(rule, updater) — who can mint new units of this resource.
  • .burnable(rule, updater) — who can burn units.
  • .recallable(rule, updater) — who can recall units from any external vault.
  • .freezable(rule, updater) — who can freeze a vault holding this resource.
  • .withdrawable(rule, updater) — who can withdraw this resource from vaults.
  • .depositable(rule, updater) — who can deposit this resource into vaults.
  • .update_metadata(rule, updater) — who can change the metadata (the token symbol always stays immutable).
  • .update_non_fungible_data(rule, updater) — who can change the mutable data on individual NFTs (non-fungible only).

A resource can also delegate its decisions to a component method rather than a static rule — see Authorization hooks.

Because caller identity is a badge, caller_component / direct_caller_template work on resource rules too. That lets the gate live on the resource rather than on each component that holds it:

// These tokens may only be withdrawn — from any vault, in any component —
// while the acting component is executing on behalf of `treasury`.
let tokens = ResourceBuilder::public_fungible()
.withdrawable(rule!(caller_component(treasury)), LOCKED)
.initial_supply(1_000_000u64);

Compare that with putting rule!(caller_component(treasury)) on a payout method of the one component that holds the vault. The method gate covers only that holder: any other component holding a vault of the same resource withdraws with no check, and the gate cannot be retrofitted onto holders you do not control. The resource rule is evaluated on every withdrawal, whichever component performs it.

The rule is checked against the frame that acts on the resource, so it reads as “the component touching these tokens was called by treasury”, not “the transaction was signed by treasury”.

UpdateRule controls who can later change an access rule, and has three variants:

UpdateRuleConstantBehaviour
UpdateRule::LockedLOCKEDNobody — not even the resource owner — can change the rule.
UpdateRule::OwnerOWNEROnly the resource owner (per the OwnerRule) can change the rule.
UpdateRule::AccessRulen/aAnyone satisfying the inner AccessRule can change the rule. No implicit owner override.

Why Locked matters — it is the only way to make a trust promise to holders that cannot be quietly retracted later. If recall is DenyAll but the updater is Owner, the owner can flip recall back on after users have already accepted the token; the same goes for freeze, mint (inflation), withdraw, and so on. Issuing a token with the recall/freeze/mint rules Locked is what lets a user verify, by reading the substate once, that those powers can never be granted to anyone — not even the issuer. Use LOCKED for any property you want holders to be able to rely on indefinitely, and reserve OWNER (or a more permissive updater) for rules you genuinely expect to govern over time.

Pass an AccessRule directly where an UpdateRule is expected and it is converted to UpdateRule::AccessRule(rule):

let admin = rule!(non_fungible(admin_badge));
let resource = ResourceBuilder::public_fungible()
// Recall is denied today, but the admin badge holder can re-enable it later.
.recallable(rule!(deny_all), admin.clone())
// Withdrawals are open, but only the owner can ever change that.
.withdrawable(rule!(allow_all), OWNER)
.build();

Calling ResourceAccessRules::new() (the default) yields safe baseline rules:

  • mint, burn, recall, freeze, update_metadata: DenyAll, updater Locked.
  • withdraw, deposit: AllowAll, updater Locked.
  • update_non_fungible_data: AllowAll, updater Owner.
  • The auth hook updater: Locked (see Authorization hooks).

Locked-by-default means a resource issued with ResourceAccessRules::new() and no overrides can never have its recall, mint, etc. silently re-enabled later. To leave room for future tightening or relaxation, pass OWNER (or an explicit access rule) as the updater on the actions you want to keep editable.

Once a resource exists, use ResourceManager::update_access_rule to change a single field. The engine consults the field’s UpdateRule to decide whether the caller is allowed:

ResourceManager::get(my_resource)
.update_access_rule(ResourceAuthAction::Withdraw, rule!(non_fungible(user_badge)));

The action is one of ResourceAuthAction::{Mint, Burn, Recall, Withdraw, Deposit, UpdateNonFungibleData, UpdateMetadata, Freeze}. If the field’s updater is Locked, this call fails even for the owner. If the updater is Owner, only the resource owner can succeed. If the updater is AccessRule(rule), only a caller satisfying rule can succeed — the owner has no implicit override.

An access rule is a static predicate. When a decision needs to look at live state — a blocklist, a per-holder cap, a paused flag — a resource can instead delegate to a hook: a method on a component that the engine calls on every mint, burn, recall, withdraw, deposit, freeze, metadata update and NFT data update. The hook takes the action and an AuthHookCaller, returns nothing, and denies by panicking.

let alloc = CallerContext::allocate_component_address(None);
let tokens = ResourceBuilder::public_fungible()
.with_authorization_hook(alloc.get_address(), "authorize")
// Without this, the hook binds for the life of the resource.
.with_authorization_hook_updater(OWNER)
.initial_supply(1_000_000u64);

The hook returns () and takes (ResourceAuthAction, AuthHookCaller); the engine rejects the resource at creation otherwise. It is skipped when the acting component is the hook’s own component, and when the hook’s component does not exist yet — which is what lets a template allocate the address, build the resource, and create the component afterwards.

A hook frame is confined to its own component: it may take &mut self and update that component’s state, and the engine refuses every other write, new substate, cross-template call, bucket and proof from inside it. That confinement is what makes a hook safe to run with the acting component’s caller badges. The engine invokes the hook on the acting component’s behalf without that component choosing to call it, so the badge satisfies the hook’s own method rule but cannot be spent on any vault or resource from inside the hook.

Because the hook runs on nearly every action, a hook that panics, denies unconditionally, or fails to decode its arguments takes the whole resource offline: no transfers, no burns, no metadata changes, and every balance in its vaults becomes unspendable. .with_authorization_hook_updater(...) is what buys the ability to recover:

// Retire a broken hook entirely...
ResourceManager::get(my_resource).set_auth_hook(None);
// ...or point the resource at a repaired one.
ResourceManager::get(my_resource)
.set_auth_hook(Some(AuthHook::new(fixed_component, "authorize".try_into().unwrap())));

The updater is Locked by default, so a hook stays permanently binding unless the issuer opts out — which is the same trust promise LOCKED makes for the access rules, and for the same reason: a holder who reads the substate can see that the hook governing their tokens can never change. Opting out has a real cost, so make it deliberately:

  • Whoever satisfies the updater can retire a hook that holders were relying on, and can give a hook-free resource a hook. A resource inspected today as unhooked is not guaranteed to stay that way.
  • The outgoing hook is not consulted when it is replaced. That is what makes a denying hook recoverable, and it means the hook cannot defend itself.
  • A replacement is validated exactly like one supplied at creation, so a mistyped method name fails the transaction rather than bricking the resource.

caller_component(...) / direct_caller_template(...) are rejected in the updater’s access rule: they always evaluate to false on a resource, so such an updater would be Locked wearing a disguise — the hook could never be repaired.

Callers are identified by various types, depending on the context:

  • ComponentAddress: When a component calls another component.
  • ResourceAddress: When a resource grants specific permissions (e.g., holding a specific NFT grants voting rights).
  • TemplateAddress: When a template has specific rights.
  • User identities: Through the transaction’s signers or proofs provided.

The auth module in tari_template_lib provides utilities for working with identities and proofs within your templates.

These identities appear in rule! requirements: component(...) / template(...) refer to the current execution scope, while caller_component(...) / direct_caller_template(...) refer to the frame’s immediate caller, carried as a virtual badge (see Caller vs. current component).

  • Principle of Least Privilege: Grant only the necessary permissions. Avoid AllowAll unless it’s genuinely intended for public methods.
  • Clear Ownership: Define and manage component/resource ownership clearly.
  • Test Thoroughly: Always write comprehensive tests to ensure your access rules behave as expected.
  • Budget for a Broken Hook: An auth hook runs on nearly every resource action, so a bug in it strands every balance of that resource. Set an updater unless the hook’s permanence is itself the guarantee you are selling.

By effectively utilizing access rules, you can ensure the security and integrity of your Tari Ootle Templates and the assets they manage.