Amazon Verified Permissions でアクセス許可ポリシー(認可)を一元管理してみよう(概要とパターン別構成編)
はじめに
アプリケーションにおける認可(誰が何をできる等)の制御は、結構考えなければならない面倒な箇所かなと思ってます。(間違える/抜け漏れるとセキュリティリスクありますし)
油断すると、認可の処理がアプリケーションコード内の色々な箇所に散らばってしまい、もう何が許可されて拒否されるのか読みづらい状況になることもまぁまぁあるかなと思います。
今回は、そのあたりを解決してくれそうな Amazon Verified Permissions なるサービスを使って、API の認可処理の構成について検討してみます。(実装を見せようかと思ったのですが、長くなったため今回は構成のみです。)
ちなみに、AWS re:Invent 2023 においても、Amazon Verified Permissions のセッションがあったようで、本記事においては、こちらの記事で触れられているセッション内の実現方法も踏まえてます。(まだ各種セッション動画は未公開)
Amazon Verified Permissions について
Amazon Verified Permissions は、簡単にいうと「アクセス許可のポリシーの一元管理」と「ポリシーに基づく承認(認可)処理」を実現してくれます。
認可処理の中身になるポリシーの設定方法としては、2 つのパターンがあるかなと思ってます。
- アプリケーション全体で静的に設定されるもの
- 例:アプリケーションには固定のロール「Owner」「Administrator」「User」があり、「Owner」は全ての Action を実行できるが、「User」は一部の Action のみ実行できる、など
- アプリケーションから動的に設定されるもの
- 例:ユーザーは他のユーザーを指定して記事を共有でき(ポリシー追加)、共有されたユーザーのみ記事を読める(ポリシー参照など認可処理部)、など
前者は、アクセス許可のポリシーを、開発者が Amazon Verified Permissions に設定します。
後者は、ユーザーの操作に応じて、アプリケーションが Amazon Verified Permissions のポリシーを設定します。
どちらの場合も、各種ユーザーアクション実行時に、アプリケーションから Amazon Verified Permissions に問い合わせて、認可処理を実現します。
Amazon Verified Permissions を使う利点
Amazon Verified Permissions を使う利点ですが、以下かなと思います。
- 認可ポリシーの一元管理(アプリケーションコードとの分離)
- 細かなアクセス制御の実現:RBAC(ロールベースのアクセス制御)や ABAC(属性ベースのアクセス制御)を含むポリシーを表現可能
特に前者の利点の効果は、コード分離によるアプリケーション開発の速度向上や、セキュリティチェックの容易さ(ポリシーが一箇所に固まっているため)、などかなぁと思いました。後者の利点は、将来的にリッチなアクセス制御する機能を追加しても拡張しやすい点ですかね。
Amazon Verified Permissions で解決する課題(例)
例えば、getArticle(id)
の処理(REST で書くと GET /articles/{id}
こんな感じ)を考えてみます。
※ 以下はすごい単純な例であり、実際の実装では API の入り口や DB の where
文などで制御する、ことが多いかなと思います。あくまでサンプルとしてご確認ください。
なお、こちらの記事 がわかりやすかったため、以下の例は、似た感じにさせてもらってます
// 基本の実装
const getArticle = (requestContext) => {
// request context で指定された id を元にデータベースに問い合わせて返却
const article = database.getArticle(requestContext.id);
return article;
};
公開記事のみ公開するようにします。
const getArticle = (requestContext) => {
const article = database.getArticle(requestContext.id);
// 以下を追加(非公開記事の場合は拒否)
if (article.status !== "public") {
throw new Error("Access Denied");
}
return article;
};
記事の所有者のみ、全公開するようにします。
const getArticle = (requestContext) => {
const article = database.getArticle(requestContext.id);
if (article.status !== "public") {
// 以下を追加(リクエスト元のユーザが記事作成者以外の場合は拒否)
if (article.author !== requestContext.user) {
throw new Error("Access Denied");
}
}
return article;
};
IP 制限をかけます。
const getArticle = (requestContext) => {
const article = database.getArticle(requestContext.id);
// 以下を追加(許可 IP アドレス以外からのリクエストの場合は拒否)
const allowIps = ["1.1.1.1", "2.2.2.2"];
if (!allowIps.contains(requestContext.ip)) {
throw new Error("Access Denied");
}
if (article.status !== "public") {
if (article.author !== requestContext.user) {
throw new Error("Access Denied");
}
}
return article;
};
と言う感じで認可処理の箇所が縦に長くなってきました。元々のアプリケーションコードが 2 行だったことを考えると、認可の実装によって、かなり読みづらくなっているといえるのではないでしょうか。
この時に、Amazon Verified Permissions を使って制御すると、アプリケーションコードとしては以下のようになります。
import { convert } from "@/utils";
const getArticle = (requestContext) => {
const article = database.getArticle(requestContext.id);
// アプリケーションコードでは以下のみ(Verified Permissions に問い合わせするのみ)
if (!avpClient.isAuthorized(convert(requestContext, article))) {
throw new Error("Access Denied");
}
return article;
};
「Owner なら許可」「公開記事なら許可」「IP アドレス x.x.x.x からのアクセスなら許可」などは全て、Amazon Verified Permissions のポリシーに記載します。これにより、アプリケーションコード内には認可のポリシーが表現されなくなり、アプリケーションは自分の処理に集中できます。
※ ちなみに、Amazon Verified Permissions へのリクエスト時に、整形する必要があるため、ある程度誤解を防ぐため、convert
なる utils を呼び出していますが、だいたいこんな感じのイメージです。
Amazon Verified Permissions を使う構成
では、実際にどのように使うかを書きます。
まずは、API の入り口の認可制御から。
API の入り口における認可処理の構成
Amazon Verified Permissions は、API Gateway や AppSync と利用可能です。
ちなみに利用可能というのは、AWS がいい感じに Verified Permissions <-> API Gateway/AppSync とサービス連携してくれるという意味ではないのでご注意ください。(コンソールでぽちぽちしたら、いい感じに連携してくれると助かるなぁと思っているけど、そうではない)
API の入り口での認可制御をする場合の構成は以下のようになります。これは AppSync も API Gateway も同じ構成です。(ちなみに Cognito を認証基盤として選択していますが、これは外部 IdP などでも可能です。そして Cognito と Amazon Verified Permissions はいい感じに連携してくれます。)
図解している流れは以下です。Verified Permissions を API Gateway(AppSync)の入り口の認可で使う場合は、全て Lambda Authorizer を使用することになります。
- User が Cognito で認証する
- Cognito は認証時に、Pre Token Generation Lambda 関数を使って ID Token をカスタマイズする(これは必須ではないが、ユーザの属性:ロールなどを ID Token に含めることで Verified Permissions の使用感が良くなる)
- Cognito 認証後、User が API にアクセスする
- API Gateway(または AppSync)の Lambda Authorizer を用いて、Lambda 関数を実行する
- Lambda 関数で、ユーザリクエストを元に、Verified Permissions に承認リクエストを送信
- Verified Permissions が Allow または Deny を返すため、その結果を元に、API に許可/拒否を返却
- 許可された場合は、API Gateway(または AppSync)から各種リソース(Lambda/DynamoDB など)にリクエスト(拒否時は 401 になる)
アプリケーション内での認可処理の構成
前述したのは API の入り口の制御でした。次はアプリケーション内の制御を書きます。
割と、API の入り口で制御できることが多いのですが、例えば、ユーザ一覧取得(GET /users
)の返却値が、ログインしているユーザのロールごとに異なる場合、などはアプリケーション内で認可制御する必要があります。(Admin
は全ユーザが見れるが、User
は一部しか見れないなど)
この場合の構成は以下のようになります。なお、Cognito 認証/Lambda Authorizer は同じため省略します。
- User が API にアクセスする
- API Gateway(または AppSync)は指定された処理(ex.
listUsers
)を呼び出す -
listUsers
の処理内(ex. Lambda 関数内)で、DynamoDB などからユーザ一覧を取得する - リストに含まれる各 User に対して Verified Permissions に承認リクエストを送信
- Verified Permissions が Allow または Deny を返すため、その結果を元に、許可された User データのみ返却値に含み返却する
また、シェアされた記事のみ表示、というような機能では以下の構成もあります。
- UserA が別の UserB に記事をシェアする(
shareArticle
) - UserB が UserA の記事を参照できる、というポリシーを Verified Permissions に登録する
- UserB がシェア記事一覧(
listSharedArticles
)をリクエストする - Verified Permissions のポリシーを参照し、シェアされている記事の情報を取得する
- DynamoDB などから、上記の記事情報をベースにデータを取得する
Verified Permissions を使う構成についての注意点
上述したように、色々な構成を書きましたが、気をつけたいのは Verified Permissions のコストです。現時点において、「100 万件の承認リクエストあたり 150 USD」かかるため、結構高いです。(Amazon Verified Permissions の料金)
キャッシュを検討したり、業務ロジックとして定義したり、DB のクエリに組み込んだり、API PATH を工夫したりすることで、承認リクエストの実行回数を減らす工夫をすることを考えたほうが良いかなと思います。簡単な認可などは、Lambda Authorizer 内の先頭とかにかくなどなども良いかなと思います。(認可処理がとっちらからないように、バランスは大事ですが)
おわりに
長くなってしまったため、一旦ここで切りますが、近々 API 入り口のやつの実装方法を書く予定です。
Discussion