Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

複雑な権限管理との格闘はもう終わり — コードに新しい風を

このページはEnglishDeutschEspañolFrançaisItalianoPortuguêsでもご覧いただけます。

By Dima KramskoyAug 5, 202612 min read
Dima Kramskoy

About Dima Kramskoy

Solutions architect who takes ideas from zero to proven. With over 20 years of software engineering, I build PoCs and MVPs on AWS that prove the path before you commit — spanning GenAI, DevOps, and FinOps. Off the clock: live-fire cooking, fishing, the outdoors, and applying Extreme Ownership to everything.

My personal page

PerfectScale™ for Kubernetes

Ready to optimize?

Get your free Kubernetes savings analysis

概要

本記事では、複雑なアプリケーション認可を管理する実用的なソリューションとしてAmazon Verified Permissions(AVP)を取り上げ、AWS PetStoreのサンプルを使った実践的なウォークスルーをお届けします。

課題

自社SaaSプロダクトのあらゆる認可ロール、ユーザー、アクターを管理しようとして、相当な複雑さに直面したことはありませんか?ビジネス要件の変更に振り回され、最適なセキュリティプリンシパルを追求しながら、変更に伴うコードレビューやA/Bテストをやりくりする苦労はおなじみでしょうか?しかも、あなたのSaaSはAWSクラウドで動いている。

そろそろ深呼吸して、一歩引いて全体を眺めてみるタイミングです!!今の解決策をもう一度、いや二度見直してみてください。比較的シンプルな選択肢をお見せできるかもしれません!複雑さの管理から解放され、リソースと労力を本当に大切なこと — つまりビジネスとビジネスニーズ — に振り向けられるようになる選択肢です!!

そのソリューションがAmazon Verified Permissionsです。


Webアプリケーションでの一般的な認可の実装方法

まず、Webアプリケーションの認可システムを整理してみましょう。こうしたシステムの必要性は、アプリケーションの進化とともに生まれます。事前に計画しておくこともできますが(システム設計の一環として計画するのが望ましいです)、それでも進化するビジネス要件に応じた変更は避けられません。本番環境へのデプロイ後も、システムのさまざまな部分を変更する必要が出てきます。

なぜこのようなシステムが必要なのでしょうか?理由はたくさんあります:

  • 特定のロールにだけアプリケーションの特定の部分を見せたい
  • 特定のロール/ユーザーによる特定の操作を制限したい
  • セキュリティ管理を強化し、不正アクセスを防ぎたい
  • 権限ロジックがコードベース中に散らばり、メンテナンスが悪夢になっている

ありがちな権限システム:絡み合った混沌

今日の認可システムの多くはこんな感じです — コード中に散らばったif/elseブロック:

// The typical nightmare in your Lambda or Node.js service:
boolean userAuthorizedForOrder(Order order, User user) {
if (user.storeId == order.storeId) {
if (user.roles.contains("store-owner")) {
return true; // store owners can access their own store's orders
} else if (user.roles.contains("customer")) {
if (user.id == order.customerId) {
return true; // customers can only see their own orders
}
}
}
return false;
}

その後どうなるでしょう?ロールをもう1つ追加します。さらにもう1つ。次は地域マネージャー向けの特殊ケース。次は24時間経過後に注文をキャンセルできるのは誰かというルール。次は週末アクセスに関する新しいビジネス要件。気づけばネストした条件分岐の海に溺れ、コードレビューは過酷になり、ビジネス要件が変わるたびにデプロイが必要になり、チームの半分は機能開発ではなくエッジケースの議論に明け暮れています。

現実のペインポイント

ロールの爆発: 2つだったロールが5つになり、10になり、ついには「週末限定で店舗従業員権限を持つ地域マネージャー」まで登場し、誰が何を持つべきか誰も把握できなくなります。

コードレビューの悪夢: 権限を変更するたびにコード変更が必要です。開発者は機能をリリースする代わりに、PRでポリシーロジックの議論に何時間も費やしています。

認可のテスト: すべての組み合わせをどうやってテストするのでしょうか?ユーザー × ロール × リソース × アクションのマトリクス = カオスです。エッジケースを見落とし、本番で障害が起きます。

監査証跡: あのユーザーがなぜあの注文にアクセスできたのか、コンプライアンス部門にうまく説明できるでしょうか。答えは5つのファイルにまたがるgit履歴の中に埋もれています。

柔軟性ゼロ: 金曜日に権限を変更したい?残念、それはデプロイ作業です。ポリシーをすぐロールバックしたい?もう一度デプロイです。


Amazon Verified Permissionsによる認可管理

では、その負担を軽くできるとしたらどうでしょう?Webアプリケーションの権限管理の大部分を肩代わりしてくれるサービスがすでにある、と言われたら?

認可をコードに埋め込むのではなく、完全に分離したらどうでしょうか?権限が宣言的で、バージョン管理され、一元化されていて、アプリケーションを再デプロイせずに変更できるとしたら?

それがAmazon Verified Permissions(AVP)の提供する価値です。

アーキテクチャ

仕組みは(大まかに)次のとおりです:

  1. ユーザーが認証 — AWS Cognito(または任意のIdP)経由
  2. APIがリクエストを受信 — JWTトークン付き
  3. Lambdaオーソライザーがリクエストをインターセプト
  4. Lambdaが認可クエリを構築 — プリンシパル(誰が)、アクション(何をするか)、リソース(何に対して)、コンテキスト(追加属性)を含む
  5. LambdaがそのクエリでAVPを呼び出す
  6. AVPがCedarポリシーを評価(認可のために設計されたポリシー言語)
  7. AVPがALLOWまたはDENYを返す
  8. API Gatewayが判定を適用 — リクエストは続行されるか403になる

ここでの要点:すべての権限ロジックはAVP内のCedarポリシーにあります。コードは「これは認可されているか?」と尋ねて、その答えを信頼するだけです。

media

実例:PetStore

サンプルアプリケーションとして、AWSが提供するPetStoreリポジトリを使用しました。説明されている機能のほとんどを含んでいますが、提供されているスキーマが動作しなかったため、若干の変更を加えています — おそらくAWSによるサービスのアップデートが原因でしょう。比較的新しいサービスであり、多くの領域でまだ発展途上であると考えてよいと思います。

提供されている手順に沿って、Amplifyアプリケーションのデプロイから始めるだけです。リポジトリを取得してAWS環境にデプロイするために、GitHubアカウントでの認証を求められる場合があります。デプロイが完了したら、上のアーキテクチャ図を見ながら、各コンポーネントとそれらの通信方法を確認できます。


セットアップ手順

ステップ1:Cognitoのユーザーとグループ

IDプロバイダーにはAWS Cognitoを使用します。デプロイ後にAmazon Cognitoのダッシュボードに移動すると、「petstoresample」で始まるユーザープールが見つかるはずです。これがあなたのプールで、このアプリケーション用のユーザーとグループをここで作成します。

3人のユーザーを作成:

このPoCでは3人のユーザーを作成する必要があります。1人はLambda関数内にハードコードされているため、必ず「abhi」という名前にしてください。残りの2人は自由です — 私は「owner」と「visitor」と名付けました。作成時の注意点:

  • メール確認は送信しない
  • 「メールアドレス確認済み」としてマークする
  • パスワードはシンプルなものにする(セットアップ後にリセットしたくないので)

すべて完了すると、次のようになるはずです:

media

ownerユーザーにカスタム属性を追加:

「owner」ユーザーにカスタム属性employmentStoreCodeを忘れずに追加してください。これは後ほどAmazon Verified Permissionsのポリシーの一部として使用します。「編集」をクリックして更新し、ユーザーの属性が次のようになるようにします:

media

グループを作成してユーザーを割り当て:

2つのグループを作成し、そこにユーザーを追加する必要があります。これらのグループ名(「Store-Owner-Role」と「Customer」)はCognitoが発行するトークンに含まれ、ロールベースアクセス制御(RBAC)に使用されます。

media

ユーザー「abhi」は、もう1人の「visitor」ユーザーと一緒に「Customer」グループに追加します:

media

そして当然、追加のタグを付与した「owner」は「Store-Owner-Role」グループに入れます:

media

ステップ2:Amazon Verified Permissionsのセットアップ

ここまで完了したら、次はAmazon Verified Permissionsの確認と設定です。このパートではLambdaオーソライザーとAPI Gatewayも確認します — これらは同じAmplifyデプロイで、アプリケーションの一部としてデプロイされています。

検索欄に入力してAmazon Verified Permissionsに移動し、まず新しいPolicy storeを作成しましょう。リポジトリではスキーマから始めることを推奨していますが、UIが変わっており、先にPolicy storeを作成する必要があります。最終的には私が提供するスキーマを使うので、内容は適当でも構いません。手間を省くために「Empty Policy Store」を指定し、「PetStore」という名前を付けるだけでOKです。

ステップ3:スキーマの定義

ここが重要です。スキーマは、アプリケーションにどのようなプリンシパル、リソース、アクション、関係が存在するかをAVPに伝えます。次のスキーマを使用してください:

{
"MyApplication": {
"actions": {
"GetOrder": {
"appliesTo": {
"principalTypes": ["User"],
"resourceTypes": ["Order"]
}
},
"GetStoreInventory": {
"appliesTo": {
"principalTypes": ["User"],
"resourceTypes": ["Application"]
}
},
"ListOrders": {
"appliesTo": {
"principalTypes": ["User"],
"resourceTypes": ["Application"]
}
},
"PlaceOrder": {
"appliesTo": {
"principalTypes": ["User"],
"resourceTypes": ["Application"]
}
},
"SearchPets": {
"appliesTo": {
"principalTypes": ["User"],
"resourceTypes": ["Application"]
}
}
},
"entityTypes": {
"Application": {
"memberOfTypes": [],
"shape": {
"attributes": {
"storeId": {"type": "String"}
},
"type": "Record"
}
},
"Order": {
"memberOfTypes": [],
"shape": {
"attributes": {
"owner": {
"name": "User",
"type": "Entity"
},
"storeId": {
"type": "String"
}
},
"type": "Record"
}
},
"Pet": {
"memberOfTypes": [],
"shape": {
"attributes": {
"owner": {
"name": "User",
"type": "Entity"
},
"storeId": {
"type": "String"
}
},
"type": "Record"
}
},
"Group": {
"memberOfTypes": [],
"shape": {
"attributes": {},
"type": "Record"
}
},
"User": {
"memberOfTypes": ["Group"],
"shape": {
"attributes": {
"employmentStoreCode": {"type": "String"}
},
"type": "Record"
}
}
}
}
}

元のプロジェクトのスキーマには小さな問題があるため、代わりにこれをPolicy Storeのスキーマにコピーしてください。

ステップ4:Cedarポリシーの作成

Policy Storeをクリックすると、左パネルに「Policies」リンクが表示されます。それをクリックし、「Create Policy > Create static policy」をクリックします。ポリシー作成ウィザードは、先ほど追加したスキーマを使用します。ここでは許可型のポリシーを使います — スコープは参加者のグループに対して定義し、リソーススコープは必要なものを選択した特定のリソースに、アクションタイプは特定のアクションセットとして選択する必要があります。

使えるちょっとしたテクニックがあります:「All principals」「All resources」「All actions」を選択して完全許可ポリシーを作成・保存し、その後編集画面でCedar言語のポリシーをそのまま貼り付ける方法です。あくまで回避策なので、ポリシー宣言の書き方を理解している必要があります。

このPoCに必要なポリシーは次のとおりです:

RBACの例 — Customerはペットの検索と注文が可能:

permit(
principal in MyApplication::Group::"Customer",
action in [MyApplication::Action::"SearchPets", MyApplication::Action::"PlaceOrder"],
resource == MyApplication::Application::"main"
);

ABACの例 — 店舗オーナーは自分の店舗の在庫のみアクセス可能:

permit(
principal in MyApplication::Group::"Store-Owner-Role",
action == MyApplication::Action::"GetStoreInventory",
resource == MyApplication::Application::"main"
)
when {
principal.employmentStoreCode == resource.storeId
};

ABACの例 — ユーザーは自分の注文のみ閲覧可能:

permit(
principal in MyApplication::Group::"Customer",
action == MyApplication::Action::"GetOrder",
resource is MyApplication::Order
)
when {
principal == resource.owner
};

Cloud bill shouldn't be a mystery

One platform for AI and Cloud optimization.

Lambdaオーソライザーとの統合

Lambda関数はAPIリクエストを受け取り、次の処理を行う必要があります:

  1. JWTトークンの検証(ユーザー情報の抽出)
  2. AVP向けの認可クエリの構築
  3. AVPのisAuthorized APIの呼び出し
  4. ALLOW/DENYの返却

実際のPetStoreリポジトリのコードは次のとおりです:

const AWS = require('aws-sdk');
const avp = new AWS.Service({
apiConfig: require('./verifiedpermissions.json')
});
const { CognitoJwtVerifier } = require("aws-jwt-verify");
const jwtVerifier = CognitoJwtVerifier.create({
userPoolId: process.env.USERPOOLID,
tokenUse: "id",
clientId: process.env.APPCLIENTID
});
const policyStoreId = process.env.POLICYSTOREID;
exports.handler = async (event) => {
// Extract and verify the JWT token
const token = event.headers["Authorization"];
let payload;
try {
payload = await jwtVerifier.verify(token);
var groups = payload["cognito:groups"] || [];
// Build the principal entity (the user)
var userEntity = {
"identifier": {
"entityType": "MyApplication::User",
"entityId": payload["cognito:username"]
},
"attributes": {
"employmentStoreCode": {
"string": payload["custom:employmentStoreCode"] == null ? "" : payload["custom:employmentStoreCode"]
}
},
"parents": []
};
// Add groups as parent entities (for group membership)
groups.forEach((group) => {
userEntity.parents.push({
"entityType": "MyApplication::Group",
"entityId": group
});
});
// Build the authorization query
let entities = {
entityList: [userEntity]
};
// Add resource entities based on the action
addResourceEntities(entities, actionMap[event.httpMethod + event.resource], event.pathParameters);
// Call AVP
let authQuery = {
policyStoreId,
principal: { "entityType": "MyApplication::User", "entityId": payload["cognito:username"] },
action: { "actionType": "MyApplication::Action", "actionId": actionMap[event.httpMethod + event.resource] },
resource: buildResource(actionMap[event.httpMethod + event.resource], event.pathParameters),
entities
};
const authResult = await avp.isAuthorized(authQuery).promise();
if (authResult.decision == 'ALLOW') {
return buildResponse(200, 'Authorized', authResult);
} else {
return buildResponse(403, "You are not authorized to perform this action.", authResult);
}
} catch(err) {
console.log(err);
return buildResponse(403, "Error: " + err);
}
};

重要なポイント: コードには権限ロジックが一切含まれていません。コンテキストをAVPに渡し、その答えを信頼するだけです。それこそがこの仕組みの核心です。


本当のメリット — 再デプロイ不要の変更

ここからが本領発揮です。

シナリオ: ビジネス側から「来月から、Store-Owner-Roleのユーザーも注文をキャンセルできるようにしてほしい。ただし、注文から24時間以上経過したものに限る」と言われたとします。

従来のハードコードされた権限の場合:

  • Lambdaコードを修正する
  • getOrderAge()関数を追加する
  • 新しいロジックのテストを書く
  • コミットしてPRを作成し、コードレビューを待つ
  • ロジックに疑問が出て、修正を繰り返す
  • マージしてステージングにデプロイし、テストする
  • 本番にデプロイする
  • バグがあれば、ビジネスが損失を出している間にもう一度デプロイする

Amazon Verified Permissionsの場合:

  • AVPコンソールにログインする
  • 新しいCedarポリシーを追加する(2分で完了):
permit(
principal in MyApplication::Group::"Store-Owner-Role",
action == MyApplication::Action::"CancelOrder",
resource is MyApplication::Order
)
when {
principal.employmentStoreCode == resource.storeId &&
resource.age > 1440 // minutes
};
  • 保存をクリック
  • 即座に反映されます。デプロイなし。コード変更なし。Lambdaの再デプロイなし。

これで完了です。コードは変わっていません。Lambdaも再デプロイしていません。認可ルールはリアルタイムで更新されました。


RBAC + ABAC:両者のいいとこ取り

Cedarでは両方のアプローチを組み合わせられます:

RBACのみ:「ユーザーはAdminグループに所属しているか?」(単純な二値判定で、リソースの属性は考慮しない)

ABACのみ:「ユーザーのdepartment属性はリソースのdepartment属性と一致するか?」(きめ細かいが、多数の属性が必要)

RBAC + ABAC(Cedarの強み):「ユーザーはStore-Owner-Roleグループに所属し、かつemploymentStoreCodeがリソースのstoreIdと一致するか?」

permit(
principal in MyApplication::Group::"Store-Owner-Role",
action == MyApplication::Action::"ManageInventory",
resource is MyApplication::Application
)
when {
principal.employmentStoreCode == resource.storeId
};

これにより、ロールベースのグルーピングと属性ベースのきめ細かさという、両者のいいとこ取りが実現します。


テストとデプロイ

ポリシーを設定したら、テストしましょう:

  1. Customerユーザーとして: SearchPetsは可能?✅ はい。GetStoreInventoryは?❌ いいえ。
  2. Store-Owner-Roleユーザーとして: 自分の店舗のGetStoreInventoryは可能?✅ はい。別の店舗では?❌ いいえ。
  3. employmentStoreCode=petstore-londonを持つownerとして: petstore-londonの注文にアクセスできる?✅ はい。

ポリシーが機能しない場合は、次を確認してください:

  • Lambda内でエンティティが正しく構造化されているか
  • ユーザーのJWTに正しいcognito:groupsが含まれているか
  • カスタム属性がユーザープロファイルに設定されているか
  • 認可クエリのエンティティタイプがスキーマと一致しているか

まとめ:配管工事から本来の目的へ

本記事は、Amazon Verified Permissionsをアプリケーションの究極の特効薬として紹介するつもりで書いたわけではありません。しかし、頻繁に変わるビジネス要件のせいで、ロールベースのWebアプリケーションのコード修正が終わらないループに陥っているなら、一度立ち止まり、視野を広げて、自分のやり方を見直してみる価値はあるかもしれません。

このサービスが時間とリソースの節約につながり、配管工事ではなくビジネスに集中する時間を生み出してくれるかもしれません。結局のところ、こうした作業はすべて配管のようなものです — アプリケーションのビジネス目標を支えてはいますが、その価値を高めることには直接つながりません。

しかし少なくともAmazon Verified Permissionsがあれば、毎週配管を修理する必要はなくなります