Identity によるアクセス制御を追加する
Dapp では、ユーザーごとに操作を制御するために、ロールベースのパーミッションが必要になることがよくあります。
このチュートリアルでは、ユーザー identity の作成と切り替え方法を解説するために、異なるロールを割り当てられたユーザーに対して、異なる挨拶を表示する簡単な Dapp を作成します。
この例では、owner
、admin
、authorized
という名前の3つのロールが存在します。
-
admin
ロールが割り当てられたユーザーには、You have a role with administrative privileges
(あなたは管理者権限のあるロールを持っています)という挨拶が表示されます。 -
authorized
ロールが割り当てられたユーザーには、Would you like to play a game?
(ゲームをしたいですか?)という挨拶が表示されます。 -
これらのロールが割り当てられていないユーザーには、
Nice to meet you!
(はじめまして!) という挨拶が表示されます。
さらに、Canister スマートコントラクトを初期化した ユーザー Identity にのみ owner
ロールが割り当てられ、owner
と admin
ロールのみ他のユーザーにロールを割り当てることができます。
大まかに言えば、各ユーザーは公開鍵/秘密鍵のペアを持ちます。公開鍵と、ユーザーがアクセスする Canister スマートコントラクト識別子を組み合わせて、セキュリティ Principal を形成し、Internet Computer 上で動作する Canister スマートコントラクトへの関数呼び出しを認証するメッセージ呼び出し元として使用できます。 下図で、ユーザー Identity がメッセージ呼び出し元を認証する方法を簡略化して示します。
始める前に
チュートリアルを始める前に、下記を確認してください。
-
ダウンロードとインストール の説明に従って DFINITY Canister SDK パッケージをダウンロードし、インストールした。
-
少なくとも1つのコマンドを実行し、
default
ユーザーの Identity が作成された。 デフォルトのユーザー Identity は、すべてのプロジェクトにおいて$HOME/.config/dfx/identity/
ディレクトリにグローバルに保存されます。 -
IDE としてVisual Studio Code を使用している場合、Install the language editor plug-in で説明したように、Motoko 用の Visual Studio Code プラグインをインストールした。
-
コンピュータ上で実行されている ローカル Canister 実行環境 プロセスをすべて停止した。
新規プロジェクトを作成する
アクセス制御とユーザー Identity の切り替えをテストするために、新規プロジェクトディレクトリを作成するには
-
ローカルコンピューターでターミナルシェルを開いてください(まだ開いていない場合)。
-
Internet Computer プロジェクトで使用しているフォルダがある場合は、そのフォルダに移動します。
-
下記コマンドを実行することで、新規プロジェクトを作成します。
dfx new access_hello
-
以下のコマンドを実行することで、プロジェクトディレクトリに移動します。
cd access_hello
デフォルトの Dapp を変更する
このチュートリアルでは、テンプレートのソースコードファイルを、ロールの割り当てと取得のための機能を持つ Dapp に置き換えます。
デフォルトの Dapp を変更するには
-
テキストエディタで
src/access_hello/main.mo
ファイルを開き、既存の内容を削除します。 -
下記のサンプルコードをコピーしてファイルに貼り付けます。
// base モジュールをインポート import AssocList "mo:base/AssocList"; import Error "mo:base/Error"; import List "mo:base/List"; shared({ caller = initializer }) actor class() { // ロールに応じた挨拶を決めて、表示する 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!"; } }; // カスタム型を定義する 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); } }; // Principal がパーミッションのあるロールを持っているかどうかを判断する 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; } }; // 不正なユーザー Identity を拒否する func require_permission(pal: Principal, perm: Permission) : async () { if ( has_permission(pal, perm) == false ) { throw Error.reject( "unauthorized" ); } }; // Princiapl に新しいロールを割り当てる 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; }; // メッセージの呼び出し元/ユーザ Identity の Principal を返す public shared({ caller }) func callerPrincipal() : async Principal { return caller; }; // メッセージの呼び出し元/ユーザーの 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
の2つのカスタム型を定義します。 -
assign_roles
関数を使用すると、メッセージの呼び出し元が Identity に関連付けられた Principal にロールを割り当てることができます。 -
callerPrincipal
関数を使用すると、Identity に関連付けられた Principal を返せます。 -
my_role
関数を使用すると、Identity に関連付けられたロールを返せます。
-
-
変更を保存し、
main.mo
ファイルを閉じて、続行します。
ローカル Canister 実行環境 を起動する
access_hello
プロジェクトをビルドする前に、開発環境で動作している ローカル Canister 実行環境 または Internet Computer のメインネットに接続する必要があります。
ローカル Canister 実行環境 を起動するには
-
ローカルコンピューターで新しいターミナルウィンドウまたはタブを開きます。
-
必要であれば、プロジェクトのルートディレクトリに移動します。
-
下記のコマンドを実行することで、コンピュータ上の ローカル Canister 実行環境 を起動します。
dfx start --background
ローカル Canister 実行環境 の起動操作が完了したら、次の手順に進めます。
Dapp を登録、ビルド、デプロイする
開発環境で動作している ローカル Canister 実行環境 に接続した後、dfx deploy
コマンドを実行することで、Dapp の登録、構築、デプロイが一度で行えます。
dfx canister create
、dfx build
、および dfx canister install
コマンドを用いてこれらの各ステップを個別に実行することも可能です。
Dapp をローカルにデプロイするには
-
必要に応じて、プロジェクトのルートディレクトリにまだいることを確認します。
-
下記のコマンドを実行することで、
access_hello
バックエンド Dapp を登録、ビルド、デプロイします。dfx deploy access_hello
Creating a wallet canister on the local network. The wallet canister on the "local" network for user "default" is "rwlgt-iiaaa-aaaaa-aaaaa-cai" Deploying: access_hello Creating canisters... Creating canister "access_hello"... "access_hello" canister created with canister id: "rrkah-fqaaa-aaaaa-aaaaq-cai" Building canisters... Installing canisters... Installing code for canister access_hello, with canister_id rrkah-fqaaa-aaaaa-aaaaq-cai Deployed canisters.
現在の Identity コンテキストを確認する
追加の Identity を作成する前に、default
Identity と default
Identity 用の Cycle ウォレットに関連付けられた Principal 識別子を確認しましょう。
Internet Computer において、Principal はユーザー、Canister スマートコントラクト、ノード、またはサブネットの内部の代表です。Principal のテキスト表現は、Principal データ型で作業している際に表示される外部識別子です。
現在の Identity と Principal を確認するには
-
下記のコマンドを実行することで、現在アクティブな Identity を確認します。
dfx identity whoami
このコマンドは、下記のような出力を表示します。
default
-
下記のコマンドを実行することで、
default
ユーザー Identity の Principal を確認します。dfx identity get-principal
このコマンドは、下記のような出力を表示します。
zen7w-sjxmx-jcslx-ey4hf-rfxdq-l4soz-7ie3o-hti3o-nyoma-nrkwa-cqe
-
下記のコマンドを実行することで、
default
ユーザー Identity に関連付けられたロールを確認します。dfx canister --wallet=$(dfx identity get-wallet) call access_hello my_role
このコマンドは、下記のような出力を表示します。
(opt variant { owner })
新しいユーザー Identity を作成する
Dapp のアクセス制御のテストを始めるために、いくつかの新しいユーザー Identity を作成し、それらのユーザーに異なるロールを割り当てましょう。
新しいユーザー Identity を作成するには
-
必要に応じて、プロジェクトのディレクトリにまだいることを確認します。
-
下記のコマンドを実行することで、新しい管理ユーザー Identity を作成します。
dfx identity new ic_admin
このコマンドは、下記のような出力を表示します。
Creating identity: "ic_admin". Created identity: "ic_admin".
-
新しいユーザー Identity がどのロールにも割り当てられていないことを確認するために、
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
ユーザー Identity を使用するために現在のアクティブ Identity コンテキストを切り替えて、ic_admin
ユーザーに関連付けられている Principal を表示します。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
Canister スマートコントラクトを呼び出すのに使用される Principal を確認します。dfx canister call access_hello callerPrincipal
このコマンドは、下記のような出力を表示します。
(principal "ryjl3-tyaaa-aaaaa-aaaba-cai")
デフォルトでは、Cycles ウォレットの識別子は、
access_hello
Canister スマートコントラクトのメソッドを呼び出すのに使用される Principal です。 しかし、アクセス制御を解説するために、Cycle ウォレットではなく、ユーザーコンテキストに関連付けられた Principal 使用したいと思います。 しかし、その前にic_admin
ユーザーに対してロールを割り当てましょう。そのためには、owner
ロールを持つdefault
ユーザー Identity に切り替える必要があります。
Identity にロールを割り当てる
ic_admin Identity に admin ロールを割り当てるには
-
下記のコマンドを実行することで、現在アクティブな Identity コンテキストを切り替えることで、
default
ユーザー Identity を使用するようにします。dfx identity use default
-
コマンドを下記のような Candid 構文で実行することで、
ic_admin
Principal に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 ハッシュを、ic_admin Identity の dfx identity get-principal コマンドによって返されるハッシュに置き換えてください。
|
+
オプションとして、コマンドを再実行して my_role
関数を呼び出し、ロールの割り当てを確認できます。
+
dfx --identity ic_admin canister call access_hello my_role
+ このコマンドは、下記のような出力を表示します。
+
(opt variant { admin })
-
先ほど
admin
ロールを割り当てたic_admin
ユーザー Identity を使用して、下記のコマンドを実行することで、greet
関数を呼び出します。dfx --identity ic_admin canister call access_hello greet "Internet Computer Admin"
このコマンドは、下記のような出力を表示します。
( "Hello, Internet Computer Admin. You have a role with administrative privileges.", )
authorized ユーザー Identity を追加する
この時点で、default
ユーザー Identity に owner
ロール、 ic_admin
ユーザー Identity に admin
ロールを割り当てました。
もう1つユーザー Identity を追加して、 authorized
ロールを割り当てましょう。
ただし、この例では、ユーザーの Principal を格納するのに環境変数を使用します。
新しい authorized ユーザー Identity を追加するには
-
必要に応じて、プロジェクトディレクトリにまだいることを確認します。
-
下記のコマンドを実行することで、新しい authorized ユーザー Identity を作成します。
dfx identity new alice_auth
このコマンドは、下記のような出力を表示します。
Creating identity: "alice_auth". Created identity: "alice_auth".
-
下記のコマンドを実行することで、現在アクティブな Identity コンテキストを切り替えて、新しい
alice_auth
ユーザー Identity を使用するようにします。dfx identity use alice_auth
-
下記のコマンドを実行することで、
alice_auth
ユーザーの Principal を環境変数に格納します。ALICE_ID=$(dfx identity get-principal)
下記のコマンドを実行することで、保存されている Principal を確認できます。
echo $ALICE_ID
このコマンドは、下記のような出力を表示します。
b5quc-npdph-l6qp4-kur4u-oxljq-7uddl-vfdo6-x2uo5-6y4a6-4pt6v-7qe
-
ic_admin
の Identity を使用して、下記のコマンドを実行することでalice_auth
にauthorized
ロールを割り当てます。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
ユーザー Identity を使用して、下記のコマンドを実行することでgreet
関数を呼び出してください。dfx canister call access_hello greet "Alice"
このコマンドは、下記のような出力を表示します。
( "Welcome, Alice. You have an authorized account. Would you like to play a game?", )
unauthorized なユーザー Identity を追加する
ここまで、特定のロールとパーミッションのあるユーザーを作成する簡単な例を見ました。 次のステップでは、ロールが割り当てられていない、または特別なパーミッションが与えられていないユーザー Identity を作成します。
unauthorized なユーザー Identity を追加するには
-
必要に応じて、プロジェクトディレクトリにまだいることを確認します。
-
必要に応じて、下記のコマンドを実行することで、現在アクティブな Identity を確認します。
dfx identity whoami
-
下記のコマンドを実行することで、新しいユーザー Identity を作成します。
dfx identity new bob_standard
このコマンドは、下記のような出力を表示します。
Creating identity: "bob_standard". Created identity: "bob_standard".
-
以下のコマンドを実行することで、
bob_standard
ユーザーの Principal を環境変数に格納します。BOB_ID=$(dfx --identity bob_standard identity get-principal)
-
ロールを割り当てるために
bob_standard
Identity を使用してみます。dfx --identity bob_standard canister call access_hello assign_role "(principal \"$BOB_ID\", opt variant{authorized})"
このコマンドは
Unauthorized
エラーを返します。 -
下記のコマンドを実行することで、
default
ユーザー Identity を使用して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
ユーザー Identity を使用してgreet
関数を呼び出します。dfx --identity bob_standard canister call access_hello greet "Bob"
このコマンドは、下記のような出力を表示します。
("Greetings, Bob. Nice to meet you!")
複数のコマンドでユーザー Identity を設定する
ここまで、個々のコマンドでユーザー Identity を作成し、切り替える方法について見てきました。 使用したいユーザー Identity を指定し、そのユーザー Identity のコンテキストで複数のコマンドを実行することも可能です。
1つのユーザー Identity で複数のコマンドを実行するには
-
必要であれば、プロジェクトディレクトリにまだいることを確認します。
-
下記のコマンドを実行することで、現在利用可能なユーザー Identity を一覧表示します。
dfx identity list
このコマンドは、現在アクティブなユーザー Identity を示すアスタリスクと共に、下記のような出力を表示します。
alice_auth bob_standard default * ic_admin
この例では、明示的に別の Identity を選択しない限り、
default
ユーザー Identity が使用されます。 -
下記のようなコマンドを実行することで、一覧から新しいユーザー Identity を選択し、それをアクティブなユーザーコンテキストにします。
dfx identity use ic_admin
このコマンドは、下記のような出力を表示します。
Using identity: "ic_admin".
dfx identity list
コマンドを再実行すると、ic_admin
ユーザー Identity が現在アクティブなユーザーコンテキストであることを示すアスタリスクが表示されます。コマンドラインで
--identity
を指定しなくても、選択したユーザー Identity を使用してコマンドを実行できるようになります。
ローカル Canister 実行環境 を停止する
Dapp の実体験と Identity の使用が終わったら、バックグラウンドで動作し続けないように ローカル Canister 実行環境 を停止できます。
ローカル Canister 実行環境 を停止するには
-
ネットワーク操作を表示するターミナルで、Control-C を押して、ローカルネットワークの処理を中断します。
-
下記のコマンドを実行することで、ローカル Canister 実行環境 を停止します。
dfx stop