Identity によるアクセス制御を追加する

Dapp では、ユーザーごとに操作を制御するために、ロールベースのパーミッションが必要になることがよくあります。

このチュートリアルでは、ユーザー identity の作成と切り替え方法を解説するために、異なるロールを割り当てられたユーザーに対して、異なる挨拶を表示する簡単な Dapp を作成します。

この例では、owneradminauthorized という名前の3つのロールが存在します。

  • admin ロールが割り当てられたユーザーには、You have a role with administrative privileges(あなたは管理者権限のあるロールを持っています)という挨拶が表示されます。

  • authorized ロールが割り当てられたユーザーには、Would you like to play a game?(ゲームをしたいですか?)という挨拶が表示されます。

  • これらのロールが割り当てられていないユーザーには、Nice to meet you!(はじめまして!) という挨拶が表示されます。

さらに、Canister スマートコントラクトを初期化した ユーザー Identity にのみ owner ロールが割り当てられ、owneradmin ロールのみ他のユーザーにロールを割り当てることができます。

大まかに言えば、各ユーザーは公開鍵/秘密鍵のペアを持ちます。公開鍵と、ユーザーがアクセスする Canister スマートコントラクト識別子を組み合わせて、セキュリティ Principal を形成し、Internet Computer 上で動作する Canister スマートコントラクトへの関数呼び出しを認証するメッセージ呼び出し元として使用できます。 下図で、ユーザー Identity がメッセージ呼び出し元を認証する方法を簡略化して示します。

principal identities

始める前に

チュートリアルを始める前に、下記を確認してください。

  • ダウンロードとインストール の説明に従って 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 の切り替えをテストするために、新規プロジェクトディレクトリを作成するには

  1. ローカルコンピューターでターミナルシェルを開いてください(まだ開いていない場合)。

  2. Internet Computer プロジェクトで使用しているフォルダがある場合は、そのフォルダに移動します。

  3. 下記コマンドを実行することで、新規プロジェクトを作成します。

    dfx new access_hello
  4. 以下のコマンドを実行することで、プロジェクトディレクトリに移動します。

    cd access_hello

デフォルトの Dapp を変更する

このチュートリアルでは、テンプレートのソースコードファイルを、ロールの割り当てと取得のための機能を持つ Dapp に置き換えます。

デフォルトの Dapp を変更するには

  1. テキストエディタで src/access_hello/main.mo ファイルを開き、既存の内容を削除します。

  2. 下記のサンプルコードをコピーしてファイルに貼り付けます。

    // 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 では、RolesPermissions の2つのカスタム型を定義します。

    • assign_roles 関数を使用すると、メッセージの呼び出し元が Identity に関連付けられた Principal にロールを割り当てることができます。

    • callerPrincipal 関数を使用すると、Identity に関連付けられた Principal を返せます。

    • my_role 関数を使用すると、Identity に関連付けられたロールを返せます。

  3. 変更を保存し、main.mo ファイルを閉じて、続行します。

ローカル Canister 実行環境 を起動する

access_hello プロジェクトをビルドする前に、開発環境で動作している ローカル Canister 実行環境 または Internet Computer のメインネットに接続する必要があります。

ローカル Canister 実行環境 を起動するには

  1. ローカルコンピューターで新しいターミナルウィンドウまたはタブを開きます。

  2. 必要であれば、プロジェクトのルートディレクトリに移動します。

  3. 下記のコマンドを実行することで、コンピュータ上の ローカル Canister 実行環境 を起動します。

    dfx start --background

    ローカル Canister 実行環境 の起動操作が完了したら、次の手順に進めます。

Dapp を登録、ビルド、デプロイする

開発環境で動作している ローカル Canister 実行環境 に接続した後、dfx deploy コマンドを実行することで、Dapp の登録、構築、デプロイが一度で行えます。 dfx canister createdfx build、および dfx canister install コマンドを用いてこれらの各ステップを個別に実行することも可能です。

Dapp をローカルにデプロイするには

  1. 必要に応じて、プロジェクトのルートディレクトリにまだいることを確認します。

  2. 下記のコマンドを実行することで、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 を確認するには

  1. 下記のコマンドを実行することで、現在アクティブな Identity を確認します。

    dfx identity whoami

    このコマンドは、下記のような出力を表示します。

    default
  2. 下記のコマンドを実行することで、default ユーザー Identity の Principal を確認します。

    dfx identity get-principal

    このコマンドは、下記のような出力を表示します。

    zen7w-sjxmx-jcslx-ey4hf-rfxdq-l4soz-7ie3o-hti3o-nyoma-nrkwa-cqe
  3. 下記のコマンドを実行することで、default ユーザー Identity に関連付けられたロールを確認します。

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

    このコマンドは、下記のような出力を表示します。

    (opt variant { owner })

新しいユーザー Identity を作成する

Dapp のアクセス制御のテストを始めるために、いくつかの新しいユーザー Identity を作成し、それらのユーザーに異なるロールを割り当てましょう。

新しいユーザー Identity を作成するには

  1. 必要に応じて、プロジェクトのディレクトリにまだいることを確認します。

  2. 下記のコマンドを実行することで、新しい管理ユーザー Identity を作成します。

    dfx identity new ic_admin

    このコマンドは、下記のような出力を表示します。

    Creating identity: "ic_admin".
    Created identity: "ic_admin".
  3. 新しいユーザー 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)
  4. 下記のコマンドを実行することで、新しい 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
  5. 下記のコマンドを実行することで、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 ロールを割り当てるには

  1. 下記のコマンドを実行することで、現在アクティブな Identity コンテキストを切り替えることで、default ユーザー Identity を使用するようにします。

    dfx identity use default
  2. コマンドを下記のような 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 })
  1. 先ほど 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 を追加するには

  1. 必要に応じて、プロジェクトディレクトリにまだいることを確認します。

  2. 下記のコマンドを実行することで、新しい authorized ユーザー Identity を作成します。

    dfx identity new alice_auth

    このコマンドは、下記のような出力を表示します。

    Creating identity: "alice_auth".
    Created identity: "alice_auth".
  3. 下記のコマンドを実行することで、現在アクティブな Identity コンテキストを切り替えて、新しい alice_auth ユーザー Identity を使用するようにします。

    dfx identity use alice_auth
  4. 下記のコマンドを実行することで、alice_auth ユーザーの Principal を環境変数に格納します。

    ALICE_ID=$(dfx identity get-principal)

    下記のコマンドを実行することで、保存されている Principal を確認できます。

    echo $ALICE_ID

    このコマンドは、下記のような出力を表示します。

    b5quc-npdph-l6qp4-kur4u-oxljq-7uddl-vfdo6-x2uo5-6y4a6-4pt6v-7qe
  5. ic_admin の Identity を使用して、下記のコマンドを実行することで alice_authauthorized ロールを割り当てます。

    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 ユーザー 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 を追加するには

  1. 必要に応じて、プロジェクトディレクトリにまだいることを確認します。

  2. 必要に応じて、下記のコマンドを実行することで、現在アクティブな Identity を確認します。

    dfx identity whoami
  3. 下記のコマンドを実行することで、新しいユーザー Identity を作成します。

    dfx identity new bob_standard

    このコマンドは、下記のような出力を表示します。

    Creating identity: "bob_standard".
    Created identity: "bob_standard".
  4. 以下のコマンドを実行することで、bob_standard ユーザーの Principal を環境変数に格納します。

    BOB_ID=$(dfx --identity bob_standard identity get-principal)
  5. ロールを割り当てるために bob_standard Identity を使用してみます。

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

    このコマンドは Unauthorized エラーを返します。

  6. 下記のコマンドを実行することで、default ユーザー Identity を使用して bob_standardowner ロールを割り当ててみます。

    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 ユーザー Identity を使用して greet 関数を呼び出します。

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

    このコマンドは、下記のような出力を表示します。

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

複数のコマンドでユーザー Identity を設定する

ここまで、個々のコマンドでユーザー Identity を作成し、切り替える方法について見てきました。 使用したいユーザー Identity を指定し、そのユーザー Identity のコンテキストで複数のコマンドを実行することも可能です。

1つのユーザー Identity で複数のコマンドを実行するには

  1. 必要であれば、プロジェクトディレクトリにまだいることを確認します。

  2. 下記のコマンドを実行することで、現在利用可能なユーザー Identity を一覧表示します。

    dfx identity list

    このコマンドは、現在アクティブなユーザー Identity を示すアスタリスクと共に、下記のような出力を表示します。

    alice_auth
    bob_standard
    default *
    ic_admin

    この例では、明示的に別の Identity を選択しない限り、default ユーザー Identity が使用されます。

  3. 下記のようなコマンドを実行することで、一覧から新しいユーザー Identity を選択し、それをアクティブなユーザーコンテキストにします。

    dfx identity use ic_admin

    このコマンドは、下記のような出力を表示します。

    Using identity: "ic_admin".

    dfx identity list コマンドを再実行すると、ic_admin ユーザー Identity が現在アクティブなユーザーコンテキストであることを示すアスタリスクが表示されます。

    コマンドラインで --identity を指定しなくても、選択したユーザー Identity を使用してコマンドを実行できるようになります。

ローカル Canister 実行環境 を停止する

Dapp の実体験と Identity の使用が終わったら、バックグラウンドで動作し続けないように ローカル Canister 実行環境 を停止できます。

ローカル Canister 実行環境 を停止するには

  1. ネットワーク操作を表示するターミナルで、Control-C を押して、ローカルネットワークの処理を中断します。

  2. 下記のコマンドを実行することで、ローカル Canister 実行環境 を停止します。

    dfx stop

もっと詳しく知りたいですか?

Identity と認証についてより詳しい情報をお探しの場合は、下記の関連資料をご覧ください。