Check if function is called from another canister

Is there a way to check if a function from an actor B is called from an actor A ?

In other words, is it possible to check if a shared({caller}) equals the principal of another canister / actor?

import Principal "mo:base/Principal";

actor B {

  public shared query({ caller = A }) func say(phrase : Text) : async Text {
    let owner : Principal = Principal.fromActor(B);

    // Restrict access to this function to a particular actor A or B?

    return phrase;
  };
};

Something like

if (owner == caller) …

should work just fine

That was also my expectation but, according my test, it seems that each actor has its own principal.

if (owner_canister_A == caller_canister_B) cannot be met.

I might have missed something of course but this other post seems to lead to the same conclusion.

It’s a bit strange to call this “owner”, self would be more idiomatic.

This code checks if the caller is yourself. If you want to check if the caller is some other canister, you have to compare caller against that… maybe your confusion is less about the comparison or the check, but rather how to get the identity (principal) of that “other” canister in the first place?

Or do you want to recognize all canisters that have the same code?

Absolutely.

I have try the following, but I guess then my error is on how to get the identity of the canisters?

// Canister A

import Principal "mo:base/Principal";

import Error "mo:base/Error";

import B "canister:B";

actor A {

  public query func say(phrase : Text) : async Text {
    let result: Text = await B.say(phrase);
    return result;
  };
};

// Canister B

actor B {

  public shared query({ caller }) func say(phrase : Text) : async Text {
    let owner : Principal = Principal.fromActor(B);

    if (owner != caller) {
        throw Error.reject("No permission");
    };

    return phrase;
  };
};

Shouldn’t that be

let owner : Principal = Principal.fromActor(A);

if you want B to check that the call comes from A?

Yes indeed, when I realized that each principals are different, that was thought too but, then, how can I access / import the principal of the caller?

I tried following:

See next post, forum don’t let me add the code here

Which leads to following compile error:

import error [M0011], canister alias “A” not defined

import Principal "mo:base/Principal";

import Error "mo:base/Error";

import A "canister:A";

actor B {

  public shared query({ caller }) func say(phrase : Text) : async Text {
    let owner : Principal = Principal.fromActor(A);

    if (owner != caller) {
        throw Error.reject("No permission");
    };

    return phrase;
  };
};

Just like you do, query({caller = name_of_the_variable_you_want_to_use}) or variants there of. The thing after query is a normal Motoko pattern (like in case), matching on a record that has a field caller

I think you did not get my question.

Sure I can get the caller but, I need to compare it to the expected canister principal, that’s what I don’t get.

In following, xxx is the fixed canister I want to allow in my function.

As displayed in my previous post, I tried to use Principal.fromActor but got a alias not defined error when try to import the expected canister.

public shared query({ caller }) func say(phrase : Text) : async Text {

    if (xxx != caller) {
        throw Error.reject("No permission");
    };

    return phrase;
  };

So not sure how to implement / code xxx.

Again, thank you for your time, sorry it takes such iterations :sweat_smile:.

Ah, you get an error message! My bad, I missed that.

Is the other canister a canister in the same dfx project? Then this should work, and maybe some dfx expert can help.

If not, then you can hardcode the principal (actor "xxx-yyy-zzz"), or you can add code to your service to set the ACL (access control list, which is what you are implementing here) later, via update calls.

Yes, same dfx project.

I can successfuly import the canister to make the call, like MyCanister.say("hello");, but, cannot use it in the Principal.fromActor(MyCanister) function.

I thought about the hard-coded principal but, did not thought about the ACL, good idea :+1:.

Nevertheless I was hoping for an easier built-in solution, if there aren’t, sure will have to follow that path.

Thank you for your time!

That is very odd indeed, and looks like a bug somewhere.

Isn’t the issue here that dfx doesn’t not support mutually recursive canisters, but requires them to be linearly dependent?

In the problematic examples, don’t both actors A and B reference each other?

I think that’s the root cause, but either Motoko or dfx’s error message could be much better…

1 Like

If you use and actor class you can set it equal to “this” in the declaration.

actor class B() = this{
///code
}

If it ain’t supported then yes, that’s probably the root cause of the error alias not defined.

So there is no other choice than hard-coding the principal of the caller canister in the receiver canister to achieve my goal?

Sounds interesting but, it is still unclear to me how a class actor can be imported in another actor, another canister, because import MyCanister "canister:MyCanister"; does not work with class actor.

Don’t you happen to have a simple example to share?

Oh, good point! Embarrassingly bad error handling then (in dfx, I assume, as moc could handle that case).

Dfx would have no problem with recursive alias handling (canister IDs are pre-allocated), the problem is “only” with the mutual dependency of

  • Passing types of imported canisters to moc
  • Obtaining inferred types from moc

So if dfx would support canisters with hand-written candid interface (good practice anyways), this could break the cycle and make this work.

One way around this is to create a type that matches the signature of your actors. Then you can reference them by type instead of class. I’m not at a computer at the moment, but can try to write something up tomorrow.

Something like

Import Atype “types”;

Let A :Atype = actor.fromPrincipal(msg.caller);

2 Likes

Ultimately I implemented it with a custom solution - i.e. I’ve got an environment modules that contains a list of canister ids and a function that checks if a caller match this list.

// env.mo
module {
    public let manager: [Text] = ["aaaaa-ziaaa-aaaai-aaafq-cai"];
}

// utils.mo
import Principal "mo:base/Principal";
import Array "mo:base/Array";

import Env "../env";

module {
    public func isManager(caller: Principal): Bool {
        hasPrivilege(caller, Env.manager);
    };

    private func hasPrivilege(caller: Principal, privileges: [Text]): Bool {
        func toPrincipal(entry: Text) : Principal {
            Principal.fromText(entry);
        };

        let principals: [Principal] = Array.map(privileges, toPrincipal);

        func filterAdmin(admin: Principal): Bool {
            admin == caller
        };

        let admin: ?Principal = Array.find(principals, filterAdmin);

        switch (admin) {
            case (null) {
                return false;
            };
            case (?admin) {
                return true;
            }
        }
    };
}

// used in one of my actor

public shared({ caller }) func doSomething(): async() {
        if (not Utils.isManager(caller)) {
            throw Error.reject("Unauthorized access. Caller is not a manager.");
        };

        ...
    };
1 Like