添加带有身份的访问控制
Dapps 通常需要基于角色的权限来控制不同用户可以执行的操作。
为了说明如何创建和切换用户身份,本教程创建了一个简单的 dapp,为分配给不同角色的用户显示不同的问候语。
在此示例中,有三个命名角色 - +owner
、+admin
和 +authorized
。
-
分配了
admin
角色的用户会看到显示您拥有具有管理权限的角色
的问候语。 -
分配了`authorized` 角色的用户会看到显示`你想玩游戏吗?` 的问候语。
-
未分配这些角色之一的用户会看到显示“很高兴认识你!”的问候语。
此外,只有初始化容器智能合约的用户身份被分配了`owner角色,并且只有
owner和
admin`角色可以将角色分配给其他用户。
在高层次上,每个用户都有一个公钥/私钥对。 公钥与用户访问的容器智能合约标识符相结合形成一个安全主体,然后可以用作消息调用者来验证对 Internet Computer 上运行的容器智能合约的函数调用。 下图提供了用户身份如何验证消息调用者的简化视图。
创建一个新项目
创建一个新的项目目录来测试访问控制和切换用户身份:
-
如果您还没有打开一个终端外壳,请在您的本地计算机上打开一个终端外壳。
-
更改为您用于 Internet Computer 项目的文件夹(如果您正在使用一个)。
-
通过运行以下命令创建一个新项目:
dfx new access_hello
-
通过运行以下命令切换到您的项目目录:
cd access_hello
修改默认dapp
在本教程中,您将使用具有分配和检索角色功能的 dapp 替换模板源代码文件。
修改默认 dapp:
-
在文本编辑器中打开
src/access_hello/main.mo
文件并删除现有内容。 -
将以下示例代码复制并粘贴到文件中:
// 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
函数使您能够返回与身份关联的角色。
-
-
保存更改并关闭
main.mo
文件以继续。
开始 local canister execution environment
在构建 access_hello
项目之前,您需要连接到在您的开发环境中运行的 local canister execution environment 或 Internet Computer 主网。
要启动 local canister execution environment:
-
在本地计算机上打开一个新的终端窗口或选项卡。
-
如有必要,导航到项目的根目录。
-
通过运行以下命令在您的计算机上启动 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:
-
如果需要,请检查您是否仍在项目的根目录中。
-
通过运行以下命令注册、构建和部署
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 上,委托人是用户、容器智能合约、节点或子网的内部代表。 主体的文本表示是您在使用主体数据类型时看到的外部标识符。
查看您当前的身份和原则:
-
通过运行以下命令验证当前活动的身份:
dfx identity whoami
该命令显示类似于以下内容的输出:
default
-
通过运行以下命令检查主体的“default”用户身份:
dfx identity get-principal
该命令显示类似于以下内容的输出:
zen7w-sjxmx-jcslx-ey4hf-rfxdq-l4soz-7ie3o-hti3o-nyoma-nrkwa-cqe
-
通过运行以下命令检查与
default
用户身份关联的角色:dfx canister --wallet=$(dfx identity get-wallet) call access_hello my_role
该命令显示类似于以下内容的输出:
(opt variant { owner })
创建一个新的用户身份
为了开始测试我们 dapp 中的访问控制,让我们创建一些新的用户身份并将这些用户分配给不同的角色。
要创建新的用户身份:
-
如果需要,请检查您是否仍在项目目录中。
-
通过运行以下命令创建新的管理用户身份:
dfx identity new ic_admin
该命令显示类似于以下内容的输出:
Creating identity: "ic_admin". Created identity: "ic_admin".
-
调用
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)
-
切换当前活动的身份上下文以使用新的
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
-
通过运行以下命令检查用于调用
access_hello
容器智能合约的主体:dfx canister call access_hello callerPrincipal
该命令显示类似于以下内容的输出:
(principal "ryjl3-tyaaa-aaaaa-aaaba-cai")
默认情况下,燃料费钱包标识符是用于调用`access_hello
容器智能合约中的方法的主体。 然而,为了说明访问控制,我们希望使用与用户上下文相关联的主体,而不是燃料费钱包。 不过,在我们开始这一步之前,让我们为“ic_admin”用户分配一个角色。 为此,我们需要切换到具有 `owner
角色的default
用户身份。
为身份分配角色
要将管理员角色分配给 ic_admin 身份:
-
通过运行以下命令,将当前活动的身份上下文切换为使用
default
用户身份:dfx identity use default
-
使用 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 })
-
通过运行以下命令,使用您刚刚分配了
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
角色。
但是,对于本示例,我们将使用环境变量来存储用户的主体。
添加新的授权用户身份:
-
如果需要,请检查您是否仍在项目目录中。
-
通过运行以下命令创建新的授权用户身份:
dfx identity new alice_auth
该命令显示类似于以下内容的输出:
Creating identity: "alice_auth". Created identity: "alice_auth".
-
通过运行以下命令,切换当前活动的身份上下文以使用新的
alice_auth
用户身份:dfx identity use alice_auth
-
通过运行以下命令将
alice_auth
用户的主体存储在环境变量中:ALICE_ID=$(dfx identity get-principal)
您可以通过运行以下命令来验证存储的主体:
echo $ALICE_ID
该命令显示类似于以下内容的输出:
b5quc-npdph-l6qp4-kur4u-oxljq-7uddl-vfdo6-x2uo5-6y4a6-4pt6v-7qe
-
通过运行以下命令,使用
ic_admin
身份将authorized
角色分配给alice_auth
:dfx --identity ic_admin canister call access_hello assign_role "(principal \"$ALICE_ID\", opt variant{authorized})"
-
调用
my_role
函数来验证角色分配。dfx --identity alice_auth canister call access_hello my_role
该命令显示类似于以下内容的输出:
(opt variant { authorized })
-
通过运行以下命令,使用您刚刚分配了
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?", )
添加未经授权的用户身份
您现在已经看到了一个创建具有特定角色和权限的用户的简单示例。 下一步是创建未分配给角色或未授予任何特殊权限的用户身份。
添加未经授权的用户身份:
-
如果需要,请检查您是否仍在项目目录中。
-
如果需要,通过运行以下命令检查您当前活动的身份:
dfx identity whoami
-
通过运行以下命令创建新的用户身份:
dfx identity new bob_standard
该命令显示类似于以下内容的输出:
Creating identity: "bob_standard". Created identity: "bob_standard".
-
通过运行以下命令将
bob_standard
用户的主体存储在环境变量中:BOB_ID=$(dfx --identity bob_standard identity get-principal)
-
尝试使用
bob_standard
身份来分配角色。dfx --identity bob_standard canister call access_hello assign_role "(principal \"$BOB_ID\", opt variant{authorized})"
此命令返回一个
unauthorized
错误。 -
通过运行以下命令,尝试使用
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
角色。 -
通过运行以下命令,使用
bob_standard
用户身份调用greet
函数:dfx --identity bob_standard canister call access_hello greet "Bob"
该命令显示类似于以下内容的输出:
("Greetings, Bob. Nice to meet you!")
为多个命令设置用户身份
到目前为止,您已经了解了如何为各个命令创建和切换用户身份。 您还可以指定要使用的用户身份,然后在该用户身份的上下文中运行多个命令。
在一个用户身份下运行多个命令:
-
如果需要,请检查您是否仍在项目目录中。
-
通过运行以下命令列出当前可用的用户身份:
dfx identity list
该命令显示类似于以下内容的输出,并带有一个星号,表示当前活动的用户身份。
alice_auth bob_standard default * ic_admin
在此示例中,除非您明确选择不同的身份,否则使用
default
用户身份。 -
从列表中选择一个新的用户身份,并通过运行类似于以下的命令使其成为活动用户上下文:
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:
-
在显示网络操作的终端中,按 Control-C 中断本地网络进程。
-
通过运行以下命令停止 local canister execution environment:
dfx stop