添加带有身份的访问控制

Dapps 通常需要基于角色的权限来控制不同用户可以执行的操作。

为了说明如何创建和切换用户身份,本教程创建了一个简单的 dapp,为分配给不同角色的用户显示不同的问候语。

在此示例中,有三个命名角色 - +owner+admin+authorized

  • 分配了 admin 角色的用户会看到显示 您拥有具有管理权限的角色 的问候语。

  • 分配了`authorized` 角色的用户会看到显示`你想玩游戏吗?` 的问候语。

  • 未分配这些角色之一的用户会看到显示“很高兴认识你!”的问候语。

此外,只有初始化容器智能合约的用户身份被分配了`owner角色,并且只有owneradmin`角色可以将角色分配给其他用户。

在高层次上,每个用户都有一个公钥/私钥对。 公钥与用户访问的容器智能合约标识符相结合形成一个安全主体,然后可以用作消息调用者来验证对 Internet Computer 上运行的容器智能合约的函数调用。 下图提供了用户身份如何验证消息调用者的简化视图。

principal identities

开始之前

在开始本教程之前,请验证以下内容:

  • 您已按照下载并安装中的说明下载并安装了 {sdk-short-name} 包。

  • 您已经运行了至少一个导致您的 default 用户身份被创建的命令。 您的默认用户身份将全局存储在 $HOME/.config/dfx/identity/ 目录中的所有项目中。

  • 如果您使用 Visual,您已按照所述为 Motoko 安装 Visual Studio Code 插件安装语言编辑器插件 Studio Code 作为您的 IDE。

  • 您已停止计算机上运行的任何 local canister execution environment 进程。

创建一个新项目

创建一个新的项目目录来测试访问控制和切换用户身份:

  1. 如果您还没有打开一个终端外壳,请在您的本地计算机上打开一个终端外壳。

  2. 更改为您用于 Internet Computer 项目的文件夹(如果您正在使用一个)。

  3. 通过运行以下命令创建一个新项目:

    dfx new access_hello
  4. 通过运行以下命令切换到您的项目目录:

    cd access_hello

修改默认dapp

在本教程中,您将使用具有分配和检索角色功能的 dapp 替换模板源代码文件。

修改默认 dapp:

  1. 在文本编辑器中打开 src/access_hello/main.mo 文件并删除现有内容。

  2. 将以下示例代码复制并粘贴到文件中:

    // Import base modules
    import AssocList "mo:base/AssocList";
    import Error "mo:base/Error";
    import List "mo:base/List";
    
    shared({ caller = initializer }) actor class() {
    
        // Establish role-based greetings to display
        public shared({ caller }) func greet(name : Text) : async Text {
            if (has_permission(caller, #assign_role)) {
                return "Hello, " # name # ". You have a role with administrative privileges."
            } else if (has_permission(caller, #lowest)) {
                return "Welcome, " # name # ". You have an authorized account. Would you like to play a game?";
            } else {
                return "Greetings, " # name # ". Nice to meet you!";
            }
        };
    
        // Define custom types
        public type Role = {
            #owner;
            #admin;
            #authorized;
        };
    
        public type Permission = {
            #assign_role;
            #lowest;
        };
    
        private stable var roles: AssocList.AssocList<Principal, Role> = List.nil();
        private stable var role_requests: AssocList.AssocList<Principal, Role> = List.nil();
    
        func principal_eq(a: Principal, b: Principal): Bool {
            return a == b;
        };
    
        func get_role(pal: Principal) : ?Role {
            if (pal == initializer) {
                ?#owner;
            } else {
                AssocList.find<Principal, Role>(roles, pal, principal_eq);
            }
        };
    
        // Determine if a principal has a role with permissions
        func has_permission(pal: Principal, perm : Permission) : Bool {
            let role = get_role(pal);
            switch (role, perm) {
                case (?#owner or ?#admin, _) true;
                case (?#authorized, #lowest) true;
                case (_, _) false;
            }
        };
    
        // Reject unauthorized user identities
        func require_permission(pal: Principal, perm: Permission) : async () {
            if ( has_permission(pal, perm) == false ) {
                throw Error.reject( "unauthorized" );
            }
        };
    
        // Assign a new role to a principal
        public shared({ caller }) func assign_role( assignee: Principal, new_role: ?Role ) : async () {
            await require_permission( caller, #assign_role );
    
            switch new_role {
                case (?#owner) {
                    throw Error.reject( "Cannot assign anyone to be the owner" );
                };
                case (_) {};
            };
            if (assignee == initializer) {
                throw Error.reject( "Cannot assign a role to the canister owner" );
            };
            roles := AssocList.replace<Principal, Role>(roles, assignee, principal_eq, new_role).0;
            role_requests := AssocList.replace<Principal, Role>(role_requests, assignee, principal_eq, null).0;
        };
    
        public shared({ caller }) func request_role( role: Role ) : async Principal {
            role_requests := AssocList.replace<Principal, Role>(role_requests, caller, principal_eq, ?role).0;
            return caller;
        };
    
        // Return the principal of the message caller/user identity
        public shared({ caller }) func callerPrincipal() : async Principal {
            return caller;
        };
    
        // Return the role of the message caller/user identity
        public shared({ caller }) func my_role() : async ?Role {
            return get_role(caller);
        };
    
        public shared({ caller }) func my_role_request() : async ?Role {
            AssocList.find<Principal, Role>(role_requests, caller, principal_eq);
        };
    
        public shared({ caller }) func get_role_requests() : async List.List<(Principal,Role)> {
            await require_permission( caller, #assign_role );
            return role_requests;
        };
    
        public shared({ caller }) func get_roles() : async List.List<(Principal,Role)> {
            await require_permission( caller, #assign_role );
            return roles;
        };
    };

    让我们看一下这个 dapp 的几个关键要素:

    • 你可能会注意到 greet 函数是你在之前的教程中看到的 greet 函数的一个变体。

      然而,在这个 dapp 中,greet 函数使用消息调用者来确定应该应用的权限,并根据与调用者关联的权限来显示要显示的问候语。

    • dapp 定义了两种自定义类型——一种用于`Roles,另一种用于Permissions`。

    • assign_roles 函数使消息调用者能够将角色分配给与身份关联的主体。

    • callerPrincipal 函数使您能够返回与身份关联的主体。

    • my_role 函数使您能够返回与身份关联的角色。

  3. 保存更改并关闭 main.mo 文件以继续。

开始 local canister execution environment

在构建 access_hello 项目之前,您需要连接到在您的开发环境中运行的 local canister execution environment 或 Internet Computer 主网。

要启动 local canister execution environment:

  1. 在本地计算机上打开一个新的终端窗口或选项卡。

  2. 如有必要,导航到项目的根目录。

  3. 通过运行以下命令在您的计算机上启动 local canister execution environment:

    dfx start --background

    local canister execution environment 完成启动操作后,您可以继续下一步。

注册、构建和部署 dapp

连接到在您的开发环境中运行的 local canister execution environment 后,您可以在一个步骤中注册、构建和部署您的 dapp dfx deploy ` 命令。 您也可以使用单独的链接单独执行这些步骤+dfx canister create+,link:../cli-reference/dfx-build{ outfilesuffix}[+dfx build+] 和+dfx canister install+` 命令。

要在本地部署 dapp:

  1. 如果需要,请检查您是否仍在项目的根目录中。

  2. 通过运行以下命令注册、构建和部署 access_hello 后端 dapp:

    dfx deploy access_hello
    在本地网络上创建钱包容器。
    用户“default”的“本地”网络上的钱包容器是“rwlgt-iiaaa-aaaaa-aaaaa-cai”
    部署:access_hello
    创建容器…
    正在创建容器“access_hello”...
    使用容器 ID 创建的“access_hello”容器:“rrkah-fqaaa-aaaaa-aaaaq-cai”
    建造容器...
    安装容器...
    使用 canister_id rrkah-fqaaa-aaaaa-aaaaq-cai 安装容器 access_hello 的代码
    部署容器。

检查当前身份上下文

在我们创建任何其他身份之前,让我们查看与您的 default 身份相关联的主体标识符以及与您的 default 身份相关的循环钱包。 在 Internet Computer 上,委托人是用户、容器智能合约、节点或子网的内部代表。 主体的文本表示是您在使用主体数据类型时看到的外部标识符。

查看您当前的身份和原则:

  1. 通过运行以下命令验证当前活动的身份:

    dfx identity whoami

    该命令显示类似于以下内容的输出:

    default
  2. 通过运行以下命令检查主体的“default”用户身份:

    dfx identity get-principal

    该命令显示类似于以下内容的输出:

    zen7w-sjxmx-jcslx-ey4hf-rfxdq-l4soz-7ie3o-hti3o-nyoma-nrkwa-cqe
  3. 通过运行以下命令检查与 default 用户身份关联的角色:

    dfx canister --wallet=$(dfx identity get-wallet) call access_hello my_role

    该命令显示类似于以下内容的输出:

    (opt variant { owner })

创建一个新的用户身份

为了开始测试我们 dapp 中的访问控制,让我们创建一些新的用户身份并将这些用户分配给不同的角色。

要创建新的用户身份:

  1. 如果需要,请检查您是否仍在项目目录中。

  2. 通过运行以下命令创建新的管理用户身份:

    dfx identity new ic_admin

    该命令显示类似于以下内容的输出:

    Creating identity: "ic_admin".
    Created identity: "ic_admin".
  3. 调用 my_role 函数查看您的新用户身份尚未分配给任何角色。

    dfx --identity ic_admin canister call access_hello my_role

    该命令显示类似于以下内容的输出:

    Creating a wallet canister on the local network.
    The wallet canister on the "local" network for user "ic_admin" is "ryjl3-tyaaa-aaaaa-aaaba-cai"
    (null)
  4. 切换当前活动的身份上下文以使用新的 ic_admin 用户身份,并通过运行以下命令显示与 ic_admin 用户关联的主体:

    dfx identity use ic_admin && dfx identity get-principal

    该命令显示类似于以下内容的输出:

    Using identity: "ic_admin".
    c5wa6-3irl7-tuxuo-4vtyw-xsnhw-rv2a6-vcmdz-bzkca-vejmd-327zo-wae
  5. 通过运行以下命令检查用于调用 access_hello 容器智能合约的主体:

    dfx canister call access_hello callerPrincipal

    该命令显示类似于以下内容的输出:

    (principal "ryjl3-tyaaa-aaaaa-aaaba-cai")

    默认情况下,燃料费钱包标识符是用于调用`access_hello容器智能合约中的方法的主体。 然而,为了说明访问控制,我们希望使用与用户上下文相关联的主体,而不是燃料费钱包。 不过,在我们开始这一步之前,让我们为“ic_admin”用户分配一个角色。 为此,我们需要切换到具有 `owner 角色的 default 用户身份。

为身份分配角色

要将管理员角色分配给 ic_admin 身份:

  1. 通过运行以下命令,将当前活动的身份上下文切换为使用 default 用户身份:

    dfx identity use default
  2. 使用 Candid 语法运行类似于以下的命令,为 ic_admin 主体分配 admin 角色:

    dfx canister --wallet=$(dfx identity get-wallet) call access_hello assign_role '((principal "c5wa6-3irl7-tuxuo-4vtyw-xsnhw-rv2a6-vcmdz-bzkca-vejmd-327zo-wae"),opt variant{admin})'
确保将 principal 哈希替换为 dfx identity get-principal 命令为 ic_admin 身份返回的哈希。

+ 或者,您可以重新运行命令以调用 my_role 函数来验证角色分配。

+

dfx --identity ic_admin canister call access_hello my_role

+ 该命令显示类似于以下内容的输出:

+

(opt variant { admin })
  1. 通过运行以下命令,使用您刚刚分配了 admin 角色的 ic_admin 用户身份调用 greet 函数:

    dfx --identity ic_admin canister call access_hello greet "Internet Computer Admin"

    该命令显示类似于以下内容的输出:

    (
      "Hello, Internet Computer Admin. You have a role with administrative privileges.",
    )

添加授权用户身份

此时,您拥有一个具有 owner 角色的 default 用户身份和一个具有 admin 角色的 ic_admin 用户身份。 让我们添加另一个用户身份并将其分配给 authorized 角色。 但是,对于本示例,我们将使用环境变量来存储用户的主体。

添加新的授权用户身份:

  1. 如果需要,请检查您是否仍在项目目录中。

  2. 通过运行以下命令创建新的授权用户身份:

    dfx identity new alice_auth

    该命令显示类似于以下内容的输出:

    Creating identity: "alice_auth".
    Created identity: "alice_auth".
  3. 通过运行以下命令,切换当前活动的身份上下文以使用新的 alice_auth 用户身份:

    dfx identity use alice_auth
  4. 通过运行以下命令将 alice_auth 用户的主体存储在环境变量中:

    ALICE_ID=$(dfx identity get-principal)

    您可以通过运行以下命令来验证存储的主体:

    echo $ALICE_ID

    该命令显示类似于以下内容的输出:

    b5quc-npdph-l6qp4-kur4u-oxljq-7uddl-vfdo6-x2uo5-6y4a6-4pt6v-7qe
  5. 通过运行以下命令,使用 ic_admin 身份将 authorized 角色分配给 alice_auth:

    dfx --identity ic_admin canister call access_hello assign_role "(principal \"$ALICE_ID\", opt variant{authorized})"
  6. 调用 my_role 函数来验证角色分配。

    dfx --identity alice_auth canister call access_hello my_role

    该命令显示类似于以下内容的输出:

    (opt variant { authorized })
  7. 通过运行以下命令,使用您刚刚分配了 authorized 角色的 alice_auth 用户身份调用 greet 函数:

    dfx canister call access_hello greet "Alice"

    该命令显示类似于以下内容的输出:

    (
      "Welcome, Alice. You have an authorized account. Would you like to play a game?",
    )

添加未经授权的用户身份

您现在已经看到了一个创建具有特定角色和权限的用户的简单示例。 下一步是创建未分配给角色或未授予任何特殊权限的用户身份。

添加未经授权的用户身份:

  1. 如果需要,请检查您是否仍在项目目录中。

  2. 如果需要,通过运行以下命令检查您当前活动的身份:

    dfx identity whoami
  3. 通过运行以下命令创建新的用户身份:

    dfx identity new bob_standard

    该命令显示类似于以下内容的输出:

    Creating identity: "bob_standard".
    Created identity: "bob_standard".
  4. 通过运行以下命令将 bob_standard 用户的主体存储在环境变量中:

    BOB_ID=$(dfx --identity bob_standard identity get-principal)
  5. 尝试使用 bob_standard 身份来分配角色。

    dfx --identity bob_standard canister call access_hello assign_role "(principal \"$BOB_ID\", opt variant{authorized})"

    此命令返回一个 unauthorized 错误。

  6. 通过运行以下命令,尝试使用 default 用户身份为 bob_standard 分配 owner 角色:

    dfx --identity default canister --wallet=$(dfx --identity default identity get-wallet) call access_hello assign_role "(principal \"$BOB_ID\", opt variant{owner})"

    此命令失败,因为无法为用户分配 owner 角色。

  7. 通过运行以下命令,使用 bob_standard 用户身份调用 greet 函数:

    dfx --identity bob_standard canister call access_hello greet "Bob"

    该命令显示类似于以下内容的输出:

    ("Greetings, Bob. Nice to meet you!")

为多个命令设置用户身份

到目前为止,您已经了解了如何为各个命令创建和切换用户身份。 您还可以指定要使用的用户身份,然后在该用户身份的上下文中运行多个命令。

在一个用户身份下运行多个命令:

  1. 如果需要,请检查您是否仍在项目目录中。

  2. 通过运行以下命令列出当前可用的用户身份:

    dfx identity list

    该命令显示类似于以下内容的输出,并带有一个星号,表示当前活动的用户身份。

    alice_auth
    bob_standard
    default *
    ic_admin

    在此示例中,除非您明确选择不同的身份,否则使用 default 用户身份。

  3. 从列表中选择一个新的用户身份,并通过运行类似于以下的命令使其成为活动用户上下文:

    dfx identity use ic_admin

    + 该命令显示类似于以下内容的输出:

    Using identity: "ic_admin".

    如果您重新运行 dfx identity list 命令,ic_admin 用户身份会显示一个星号,表示它是当前活动的用户上下文。

    您现在可以使用选定的用户身份运行命令,而无需在命令行上指定 --identity

停止 local canister execution environment

在您完成对 dapp 的试验并使用身份之后,您可以停止 local canister execution environment,使其不会继续在后台运行。

要停止 local canister execution environment:

  1. 在显示网络操作的终端中,按 Control-C 中断本地网络进程。

  2. 通过运行以下命令停止 local canister execution environment:

    dfx stop

想了解更多?

如果您正在寻找有关身份和身份验证的更多信息,请查看以下相关资源: