電通総研 テックブログ

電通総研が運営する技術ブログ

Amazon Verified Permissions のポリシーは CDK で管理するのが便利

こんにちは。X(クロス)イノベーション本部 ソフトウェアデザインセンター セキュリティグループの耿です。
2023年6月に Amazon Verified Permissions というサービスがGAしましたが、まだ利用している方は少ないのではないでしょうか。

アプリケーションとは切り離して認可ポリシーを管理できるこのサービスですが、ではどうやってポリシーやスキーマを管理したら良いか考えたところ、AWS CDK が便利だったので、今回の記事ではその方法を紹介します。

Amazon Verified Permissions とは

ポリシーを事前に登録することで、アプリケーションの認可の判定を、アプリケーションの外側で行うことができるマネージドサービスです。
ポリシーは Cedar という DSL で記述します。例えば次のようなポリシー:

permit (
  principal in MyApp::User::"alice",
  action in [MyApp::Action::"GET"],
  resource in MyApp::Album::"animals"
);

これは alice というユーザーに animals というアルバムへの GET アクセスを permit (許可する)という意味になります。
principal 「誰に」、 resource 「どのリソースに対する」、 action 「どんな操作」を許可・拒否するかを基本的には書いていくことになります。

定義したポリシーに対して、アプリケーションコードから認可結果を判定させるリクエストを送信できます。
例として次のように AWS SDK for JavaScriptIsAuthorizedCommand を使って、特定の状況における認可を判定させることができます。

// authorize.ts
import { VerifiedPermissionsClient, IsAuthorizedCommand } from "@aws-sdk/client-verifiedpermissions";

const client = new VerifiedPermissionsClient({ region: "ap-northeast-1" });

const command = new IsAuthorizedCommand({
  policyStoreId: "DUMMYSTOREID",
  principal: { entityType: "MyApp::User", entityId: "alice" },
  action: { actionType: "MyApp::Action", actionId: "GET" },
  resource: { entityType: "MyApp::Album", entityId: "animals" },
});
client.send(command).then((result) => {
  console.log(result.decision); // ALLOW
});

この場合ユーザーは alice、操作対象のリソースは animals というアルバム、操作は GET なので、先ほど例に示した許可ポリシーに当てはまり、コマンドの結果の decision プロパティには ALLOW が返ってきます。(拒否される場合は DENY が返ります。)
この結果を見て、後続の処理は完全にアプリケーション側の裁量で行うことになります。
principal を Cognito と連携したり、principalresource に属性を追加したり、context でコンテキストを追加したりすることもできますが、今回はこれぐらいのシンプルな説明に留めておきます。)

スキーマ

Amazon Verified Permissions にはスキーマと呼ばれる概念があります。スキーマは前述の principalresourceaction のフォーマットを定義するものです。言葉で説明しても理解が難しいと思うので、実際にマネジメントコンソールで見てみると分かりやすいと思います。

スキーマの画面

定義済みのスキーマの画面です。principalresource で使った UserAlbum は「エンティティタイプ」セクションで定義し、action で使った GET は「アクション」セクションで定義します。マネジメントコンソールで追加したスキーマ定義は JSON フォーマットと相互変換可能であり、上のスキーマ定義は次の JSON と同等です。(画面の「JSONモード」から確認でき、決まったフォーマットが使われます)

{
    "MyApp": {
        "actions": {
            "GET": {
                "appliesTo": {
                    "context": {
                        "attributes": {},
                        "type": "Record"
                    },
                    "resourceTypes": [
                        "Album"
                    ],
                    "principalTypes": [
                        "User"
                    ]
                }
            }
        },
        "entityTypes": {
            "User": {
                "memberOfTypes": [],
                "shape": {
                    "attributes": {},
                    "type": "Record"
                }
            },
            "Album": {
                "shape": {
                    "type": "Record",
                    "attributes": {}
                },
                "memberOfTypes": []
            }
        }
    }
}

Amazon Verified Permissions を CDK でデプロイする

Amazon Verified Permissions のスキーマJSON)とポリシー(Cedar)はいずれも宣言型のコードで表現されるので、そのデプロイを CDK で行うと相性が良いのではないかと思いました。そう考えた理由は以下です。

  • 認可のためのスキーマやポリシーは、アプリケーションデータのように頻繁に変わるわけではない
  • そのためコードリポジトリで管理し、レビューと変更を確認できる状態にしたい
  • コードリポジトリで管理するなら、デプロイも自動化したい
  • IaC でデプロイしよう

まず、Amazon Verified Permissions のリソース構築は CloudFormation ではサポートされています。しかし、JSON のスキーマ定義Cedar のポリシー定義を String で記載する必要があり、CloudFormation テンプレートで管理するのはつらそうです。そこでプログラミング言語で IaC を実現できる CDK を活用して、管理しやすくしてみましょう。

ディレクトリ構成

以下の CDK アプリのリポジトリ構成で話を進めます。(重要なファイル/ディレクトリ以外は省略しています。)

sample-app/
 ├ .vscode/
 │ └ settings.json      <- VS Code で Cedar を書きやすくするための設定ファイル
 ├ bin/
 │ └ my-app.ts
 ├ cedar/
 │ ├ 0001.cedar         <- ポリシーその1
 │ ├ 0002.cedar         <- ポリシーその2
 │ └ cedarschema.json   <- スキーマ定義
 ├ lib/
 │ ├ constructs/
 │ │  └ my-construct.ts <- Verified Permissionsリソースをデプロイするコンストラクト
 │ └ my-stack.ts
 ├ cdk.context.json
 ├ cdk.json
 ├ package.json
 ├ tsconfig.json
 └ yarn.lock

Cedar 言語の VS Code 拡張

Cedar 言語を書きやすくするための VS Code 拡張があります。とりあえず入れておきましょう。
https://marketplace.visualstudio.com/items?itemName=cedar-policy.vscode-cedar

続いて .vscode/settings.json に以下の設定を追加します。ポリシーやスキーマ定義の文法が正しくないときや、スキーマ定義に一致しないポリシーを書いたときにエラーを出してくれるようになります。保存時にファイルのフォーマットもそろえてくれます。

// .vscode/settings.json
{
  "[cedar]": {
    "editor.tabSize": 2,
    "editor.wordWrapColumn": 80,
    "editor.formatOnSave": true,
    "editor.defaultFormatter": "cedar-policy.vscode-cedar",
  },
  "cedar.schemaFile": "/cedar/cedarschema.json",
  "cedar.autodetectSchemaFile": true,
}

スキーマ定義

前述の例の JSON スキーマ定義をそのまま cedar/cedarschema.json に書きます。cedarschema.json もしくは *.cedarschema.json というファイル名にすることで、 Cedar の JSON スキーマファイルとして VS Code 拡張に認識されるようになります。

// cedar/cedarschema.json
{
    "MyApp": {
        "actions": {
            "GET": {
                "appliesTo": {
                    "context": {
                        "attributes": {},
                        "type": "Record"
                    },
                    "resourceTypes": [
                        "Album"
                    ],
                    "principalTypes": [
                        "User"
                    ]
                }
            }
        },
        "entityTypes": {
            "User": {
                "memberOfTypes": [],
                "shape": {
                    "attributes": {},
                    "type": "Record"
                }
            },
            "Album": {
                "shape": {
                    "type": "Record",
                    "attributes": {}
                },
                "memberOfTypes": []
            }
        }
    }
}

ポリシー

ポリシーを2つ追加してみます。*.cedar 拡張子のファイル名が Cedar のポリシーファイルとして VS Code 拡張に認識されます。1つの *.cedar ファイルには1つのポリシーしか書けないので、2つのファイルを追加します。
1つ目は alice というユーザーに animals というアルバムへの GET アクセスを許可するポリシーです。(cedar/0001.cedar)
コメントも自由に書けるので、ポリシーの説明などを記載しておくと後から分かりやすいでしょう。

// alice に animals アルバムへのアクセスを許可する
permit (
  principal in MyApp::User::"alice",
  action in [MyApp::Action::"GET"],
  resource in MyApp::Album::"animals"
);

2つ目は bob というユーザーに flowers というアルバムへの GET アクセスを許可するポリシーです。(cedar/0002.cedar)

// bob に flowers アルバムへのアクセスを許可する
permit (
  principal in MyApp::User::"bob",
  action in [MyApp::Action::"GET"],
  resource in MyApp::Album::"flowers"
);

CDK コンストラク

いよいよ Verified Permissions のリソースを CDK でデプロイするコンストラクトです。
CDK v2.94.0 では Verified Permissions のコンストラクトは4つ利用できますが、L2 コンストラクトはなく、いずれも L1 です。

  • CfnPolicyStore
  • CfnPolicy
  • CfnPolicyTemplate
  • CfnIdentitySource

今回は上の2つを利用します。

まずは Verified Permissions のポリシーストアを作成します。

// lib/constructs/my-construct.ts
import * as fs from "fs";
import * as path from "path";
import * as vp from "aws-cdk-lib/aws-verifiedpermissions";
import { Construct } from "constructs";

export class VerifiedPermissionsConstruct extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    const cedarDir = path.join(__dirname, "..", "..", "cedar");
    // スキーマ定義をファイルから読み込む
    const schema = fs.readFileSync(path.join(cedarDir, "cedarschema.json"), "utf-8");
    // ポリシーストア
    const policyStore = new vp.CfnPolicyStore(this, "PolicyStore", {
      validationSettings: { mode: "STRICT" },
      schema: { cedarJson: schema },
    });

    // ...
  }
}

スキーマ定義はポリシーストア作成時に渡す必要があり、fs モジュールの readFileSync メソッドで先ほど定義した cedarschema.json ファイルを読み込ませています。

デプロイすると、マネジメントコンソールでも作成されたポリシーストアを確認できました。ポリシーストア ID は自動で採番されます。

作成されたポリシーストア

続いてポリシーを追加していきます。以下のコードは、 cedar ディレクトリにある *.cedar ファイルのみをループして読み込んで、ファイルの数だけポリシーをポリシーストアに追加しています。

// lib/constructs/my-construct.ts
export class VerifiedPermissionsConstruct extends Construct {
  constructor(scope: Construct, id: string) {
    // ...

    // cedar ディレクトリのファイル一覧を読み込む
    const files = fs.readdirSync(cedarDir);
    files
      .filter((file) => file.endsWith(".cedar")) // .cedar ファイルのみを抽出
      .map((file) => {
        const content = fs.readFileSync(path.join(cedarDir, file), "utf-8");
        // ポリシー
        new vp.CfnPolicy(this, `Policy-${file.split(".")[0]}`, {
          policyStoreId: policyStore.attrPolicyStoreId, // ポリシーストア ID を参照
          definition: { static: { statement: content } }, // 静的ポリシーを追加
        });
      });    
  }
}

ポリシーのリソースIDはファイル名を利用して付けることで重複がないようにしています。また map 関数で cedar ディレクトリ内の全 Cedar ファイルをループして処理することで、ポリシーを追加・削除したいときは cedar ディレクトリ内を変更するだけでよく、CDK コードの修正は必要ありません。このようなことができるのもプログラミング言語で処理を書ける CDK ならではの強みです。

デプロイすると、定義した通りのポリシーがコンソールでも確認できました。ポリシー ID は自動で採番されています。

作成されたポリシー

以上で CDK を使った Verified Permission の構築は完了です。

SDKで認可を判定させてみた

構築した Verified Permissions を確認するために、簡単にローカル環境から SDK でリクエストを投げてみます。

// authorize.ts
import { VerifiedPermissionsClient, IsAuthorizedCommand } from "@aws-sdk/client-verifiedpermissions";

const client = new VerifiedPermissionsClient({ region: "ap-northeast-1" });

const command = new IsAuthorizedCommand({
  policyStoreId: "DUMMYSTOREID", // マネジメントコンソールから確認できるポリシーストアID
  principal: { entityType: "MyApp::User", entityId: "alice" },
  action: { actionType: "MyApp::Action", actionId: "GET" },
  resource: { entityType: "MyApp::Album", entityId: "animals" },
});
client.send(command).then((result) => {
  console.log(result.decision); // ALLOW
  console.log(JSON.stringify(result.determiningPolicies)); // [{"policyId":"JLu29q39V62CChNcpFgk3x"}]
});

ユーザーが alice、リソースが animals アルバムの場合、判定結果は ALLOW となり、どのポリシーで判定されたのかも determiningPolicies プロパティで確認できます。

今度は alice に許可していない flowers アルバムへの認可を判定させてみます。

// authorize.ts
const command = new IsAuthorizedCommand({
  policyStoreId: "DUMMYSTOREID",
  principal: { entityType: "MyApp::User", entityId: "alice" },
  action: { actionType: "MyApp::Action", actionId: "GET" },
  resource: { entityType: "MyApp::Album", entityId: "flowers" },
});
client.send(command).then((result) => {
  console.log(result.decision); // DENY
  console.log(JSON.stringify(result.determiningPolicies)); // []
});

どのポリシーにも当てはまらないため determiningPolicies は空の配列であり、判定結果はデフォルトの DENY となりました。

ちなみに GetPolicyCommand を使うことで、ポリシーの内容を SDK からも取得することができます。

// authorize.ts
const command = new GetPolicyCommand({
  policyStoreId: "DUMMYSTOREID",
  policyId: "JLu29q39V62CChNcpFgk3x",
});
client.send(command).then((result) => {
  console.log(result.definition?.static?.statement);
});

// (結果)

// // alice に animals アルバムへのアクセスを許可する
// permit (
//   principal in MyApp::User::"alice",
//   action in [MyApp::Action::"GET"],
//   resource in MyApp::Album::"animals"
// );

さいごに

まだまだ利用が少ないと思われる Amazon Verified Permissions ですが、CDK を使って管理・デプロイすることと相性が良く、使ってみたいという気持ちになりました。DSL なので提供されている VS Code 拡張を利用したいですし、そうするとファイルの読み込みや、ポリシーファイルの数だけループ処理ができる CDK でデプロイするのが最も便利だと感じました。
Amazon Verified Permissions 自体も、工夫次第では様々な使い方ができそうなので、今後が楽しみなサービスです。


私たちは同じチームで働いてくれる仲間を大募集しています!たくさんのご応募をお待ちしています。

セキュリティエンジニア

執筆:@kou.kinyo、レビュー:@yamashita.tsuyoshi
Shodoで執筆されました