Caller identification
Overview
Motoko’s shared functions support a simple form of caller identification that allows you to inspect the ICP principal associated with the caller of a function. Principals are a value that identifies a unique user or canister.
You can use the principal associated with the caller of a function to implement a basic form of access control in your program.
Using caller identification
In Motoko, the shared
keyword is used to declare a shared function. The shared function can also declare an optional parameter of type {caller : Principal}
.
To illustrate how to access the caller of a shared function, consider the following:
shared(msg) func inc() : async () {
// ... msg.caller ...
}
In this example, the shared function inc()
specifies a msg
parameter, a record, and the msg.caller
accesses the principal field of msg
.
The calls to the inc()
function do not change. At each call site, the caller’s principal is provided by the system, not the user. The principal cannot be forged or spoofed by a malicious user.
To access the caller of an actor class constructor, you use the same syntax on the actor class declaration. For example:
shared(msg) actor class Counter(init : Nat) {
// ... msg.caller ...
}
Adding access control
To extend this example, assume you want to restrict the Counter
actor so it can only be modified by the installer of the Counter
. To do this, you can record the principal that installed the actor by binding it to an owner
variable. You can then check that the caller of each method is equal to owner
like this:
shared(msg) actor class Counter(init : Nat) {
let owner = msg.caller;
var count = init;
public shared(msg) func inc() : async () {
assert (owner == msg.caller);
count += 1;
};
public func read() : async Nat {
count
};
public shared(msg) func bump() : async Nat {
assert (owner == msg.caller);
count := 1;
count;
};
}
In this example, the assert (owner == msg.caller)
expression causes the functions inc()
and bump()
to trap if the call is unauthorized, preventing any modification of the count
variable while the read()
function permits any caller.
The argument to shared
is just a pattern. You can rewrite the above to use pattern matching:
shared({caller = owner}) actor class Counter(init : Nat) {
var count : Nat = init;
public shared({caller}) func inc() : async () {
assert (owner == caller);
count += 1;
};
// ...
}
Simple actor declarations do not let you access their installer. If you need access to the installer of an actor, rewrite the actor declaration as a zero-argument actor class instead.
Recording principals
Principals support equality, ordering, and hashing, so you can efficiently store principals in containers for functions such as maintaining an allow or deny list. More operations on principals are available in the principal base library.
The data type of Principal
in Motoko is both sharable and stable, meaning you can compare Principal
s for equality directly.
Below is an example of how you can record and compare principals.
import Principal "mo:base/Principal";
import HashMap "mo:base/HashMap";
import Hash "mo:base/Hash";
import Error "mo:base/Error";
actor {
// Initialize a stable variable to store principals
private stable var principalEntries : [(Principal, Bool)] = [];
// Create HashMap to store principals
private var principals = HashMap.HashMap<Principal, Bool>(
10,
Principal.equal,
Principal.hash
);
// Check if principal is recorded
public shared query(msg) func isRecorded() : async Bool {
let caller = msg.caller;
switch (principals.get(caller)) {
case (?exists) { exists };
case null { false };
};
};
// Record a new principal
public shared(msg) func recordPrincipal() : async Bool {
let caller = msg.caller;
if (Principal.isAnonymous(caller)) {
throw Error.reject("Anonymous principal not allowed");
};
principals.put(caller, true);
true;
};
};