こんにちは。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 JavaScript の IsAuthorizedCommand
を使って、特定の状況における認可を判定させることができます。
// 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 と連携したり、principal
や resource
に属性を追加したり、context
でコンテキストを追加したりすることもできますが、今回はこれぐらいのシンプルな説明に留めておきます。)
スキーマ
Amazon Verified Permissions にはスキーマと呼ばれる概念があります。スキーマは前述の principal
、resource
、action
のフォーマットを定義するものです。言葉で説明しても理解が難しいと思うので、実際にマネジメントコンソールで見てみると分かりやすいと思います。
定義済みのスキーマの画面です。principal
と resource
で使った User
や Album
は「エンティティタイプ」セクションで定義し、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で執筆されました)