こんにちは。X(クロス) イノベーション 本部 ソフトウェアデザインセンター セキュリティグループの耿です。 2023年6月に Amazon Verified Permissions というサービスがGAしましたが、まだ利用している方は少ないのではないでしょうか。 アプリケーションとは切り離して認可ポリシーを管理できるこのサービスですが、ではどうやってポリシーや スキーマ を管理したら良いか考えたところ、 AWS CDK が便利だったので、今回の記事ではその方法を紹介します。 Amazon Verified Permissions とは スキーマ Amazon Verified Permissions を CDK でデプロイする ディレクトリ構成 Cedar 言語の VS Code 拡張 スキーマ定義 ポリシー CDK コンストラクト SDKで認可を判定させてみた さいごに 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 で執筆されました )