Web APIのアクセスコントローラーとしてのAmazon Verified Permissionsの可能性を探る
Amazon Verified Permissions(以下AVP)は、2023年にGA(一般提供開始)されてから2年程経過していますが、日本語での技術記事や実践例などあまり見かけません。
私自身も最近までその存在を知らず、偶然見つけて興味を持ち、実際に検証してみました。
本記事では、AVPがWeb APIのアクセス制御を実装するサービスとしてどのような可能性を持つのか、実際に触れてみた経験をもとに、技術的な観点や所感を交えて紹介してみたいと思います!
AVPとは?
AVP(Amazon Verified Permissions)は、Amazonが提供するクラウド型のアクセスコントロールサービスです。
アプリケーションの認可処理のアクセス制御部分を柔軟かつ強力に実現するための主要な機能を備えています。
- ポリシー言語(Cedar)による柔軟なルール記述
- ポリシーストア(ポリシーの一元管理・バージョン管理)
- ポリシーエンジン(高速な評価・判定処理)
AVPの最大の特徴は、**Cedar**によるポリシーベース(PBAC: Policy-Based Access Control)のきめ細かいアクセス制御(Fine-Grained Access Control)を実現できる点です。
CedarはAmazonが開発したポリシー言語であり、アクセス制御のルールを人間が理解しやすい形で記述できることが特徴です。
Amazon発のポリシー言語ということで、IAMポリシーに似た構造を持っています。
IAMポリシーはAWSサービスのアクセス制御に特化していますが、Cedarはより汎用的なポリシー言語として設計されています。
Cedar自体はオープンソースとして公開されており、AVPはこのCedarを基盤としたアクセス制御サービスです。
AVPの所感をつかみたい人は、下記の動画のチュートリアルを参考にすると良いです。
https://www.youtube.com/watch?v=OBrSrzfuWhQ
Cedarとは?
Cedarは、Amazonが開発したポリシー言語およびその評価エンジンです。
Cedarの最大の特徴は、アクセス制御のルール(ポリシー)を人間が理解しやすい形で記述でき、かつ機械的に検証できる点にあります。
ポリシーをCedar言語で記述し、Cedarエンジンで評価することで、柔軟かつ堅牢なアクセス制御を実現します。
Cedarの基本的な構造は、以下の3つの要素で構成されています。
- Principal: アクセスを要求する主体(ユーザーやグループなど)
- Action: 実行される操作(操作の種類やメソッド)
- Resource: アクセス対象のリソース (対象のリソースやデータ)
例えば、以下のようなCedarポリシーを記述できます。
permit (
principal in PhotoFlash::User::"Alice",
action in PhotoFlash::Action::"SharePhotoLimitedAccess",
resource in PhotoFlash::Photo::"VacationPhoto94.jpg"
);
この例では、「Alice」というユーザー(prncipal)が「VacationPhoto94.jpg」という写真(resource)に対して「SharePhotoLimitedAccess」という操作(action)を実行できることを許可「permit」しています。
これらの組み合わせにより、Cedarでは下記のようなアクセス制御モデルを実現可能です。
- RBAC(Role-Based Access Control):ユーザーの役割に基づくアクセス制御
- ABAC(Attribute-Based Access Control):ユーザーやリソースの属性に基づくアクセス制御
- ReBAC(Relationship-Based Access Control):ユーザー間の関係性に基づくアクセス制御
近年では、Auth0が主体となってオープンソース化されたOpenFGAなど、他のFGAC(Fine-Grained Access Control)なども注目されています。
こういった流れを見ると、CognitoやAuth0などの認証基盤側が提供するアクセスコントロールソリューションでは限界があり、その部分を外出しした形でのアクセス制御のレイヤーが求められている印象を受けます。
やってみる
ここからは、実際にAVPを使ってWeb APIのアクセス制御を実装する例を紹介します。
今回は、以下のようなユースケースを想定しました。
- ユースケース
- Web APIを提供するマルチテナントアプリケーション
- 1ユーザーが複数テナントに所属可能
- 各テナントに紐づくリソースへのアクセス制御
- ユーザーごとに所属テナントのリソースのみアクセス可能
- client credentils flowを利用したマシン間通信(M2M)のアクセス制御
具体的には、次のようなWeb APIのエンドポイントを用意します。
GET /items
- 全アイテムの一覧を取得するAPI
- 全ユーザー/クライアントがアクセス可能(ただし所属テナントの範囲に限定)
GET /tenants/{tenant_id}/items
- 特定テナントのアイテム一覧を取得するAPI
- 指定したテナントID
tenant_id
に所属している場合のみアクセス可能
POST /tenants/{tenant_id}/items
- 特定テナントにアイテムを追加するAPI
- 指定したテナントID
tenant_id
に所属している場合のみアクセス可能
下準備
今回は、API Gateway(REST API)+Lambda関数コンテナ+AVP+Cognito User Poolという構成を採用し、インフラのプロビジョニングにはTerraformを利用します。
また、アプリケーションフレームワークにはFastAPIを使用し、Pythonで実装します。
一通りのリソースとコードのデプロイは、こちらのGitHubリポジトリを参考にしてください。
ここでは、Cedarのスキーマやポリシーの定義、FastAPIのコード、Terraformの設定ファイルなどを主要な部分に絞って紹介します。
Cedar schema
まずは、Cedarのスキーマを定義します。
スキーマは、ポリシーストアのCedarポリシーで使用するエンティティやアクションの型を定義するものです。
正しい表現か分かりませんが、DBのテーブルスキーマのようなものです。
今回は、AVPがデモで紹介されている情報を参考に、以下のようなスキーマを定義しました。
注目すべきは、
entityTypes
にUser
とTenant
の親子関係を定義している点actions
にAPIのパスをそのまま定義している点- Resourceは基本的には利用していない点
の3点です。
後述でも解説しますが、今回のポリシーでは、Resourceは基本的には利用せず、Actionのパスに基づいてアクセス制御を行います。
{
"FastapiApp": {
"commonTypes": {
"PersonType": {
"type": "Record",
"attributes": {
"email": {
"type": "String"
}
}
},
"ContextType": {
"type": "Record",
"attributes": {
"authenticated": {
"type": "Boolean",
"required": true
}
}
}
},
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"sub": {
"type": "String"
},
"personInformation": {
"type": "PersonType"
}
}
},
"memberOfTypes": [
"Tenant"
]
},
"Client": {
"shape": {
"type": "Record",
"attributes": {}
}
},
"Tenant": {
"shape": {
"type": "Record",
"attributes": {}
}
},
"Application": {
"shape": {
"type": "Record",
"attributes": {}
},
"memberOfTypes": [
"Tenant"
]
}
},
"actions": {
"get /tenants/{tenant_id}/items": {
"appliesTo": {
"principalTypes": [
"Tenant",
"Client"
],
"resourceTypes": [
"Application"
],
"context": {
"type": "ContextType"
}
}
},
"post /tenants/{tenant_id}/items": {
"appliesTo": {
"principalTypes": [
"Tenant",
"Client"
],
"resourceTypes": [
"Application"
],
"context": {
"type": "ContextType"
}
}
},
"get /items": {
"appliesTo": {
"principalTypes": [
"Tenant",
"Client"
],
"resourceTypes": [
"Application"
],
"context": {
"type": "ContextType"
}
}
}
}
}
}
コード: https://github.com/seiichi1101/avp-sample-with-fastapi/blob/main/terraform/cedar/cedarschema.json
Policy1
スキーマが定義できたら、次はポリシーを登録します。
こちらは、get /items
のAPIに対して、認証済みのユーザーであればアクセスを許可するポリシーです。
permit (
principal,
action in FastapiApp::Action::"get /items",
resource
);
コード: https://github.com/seiichi1101/avp-sample-with-fastapi/blob/main/terraform/cedar/policy1.cedar
Policy2
つづいて、特定のテナントclassmethod
に所属するユーザーが、そのテナントのリソースに対して特定の操作(一覧取得・追加)を行えることを許可するポリシーです。
一見、when
句が冗長に見えますが、resource in FastapiApp::Tenant::"classmethod"
の部分が重要で、これを追加することで、classmethod
テナントに所属するユーザーが他のテナントのリソースにアクセスできないように制御しています。
permit (
principal in FastapiApp::Tenant::"classmethod",
action in
[FastapiApp::Action::"get /tenants/{tenant_id}/items",
FastapiApp::Action::"post /tenants/{tenant_id}/items"],
resource
)
when
{
principal in FastapiApp::Tenant::"classmethod" &&
resource in FastapiApp::Tenant::"classmethod"
};
コード: https://github.com/seiichi1101/avp-sample-with-fastapi/blob/main/terraform/cedar/policy2.cedar
Policy3
Policy2と同様に、特定のテナントannotation
に所属するユーザーが、そのテナントのリソースに対して特定の操作(一覧取得・追加)を行えることを許可するポリシーです。
permit (
principal in FastapiApp::Tenant::"annotation",
action in
[FastapiApp::Action::"get /tenants/{tenant_id}/items",
FastapiApp::Action::"post /tenants/{tenant_id}/items"],
resource
)
when
{
principal in FastapiApp::Tenant::"annotation" &&
resource in FastapiApp::Tenant::"annotation"
};
コード: https://github.com/seiichi1101/avp-sample-with-fastapi/blob/main/terraform/cedar/policy3.cedar
Policy4 by Policy Template
ここでは、AVPのポリシーテンプレートを利用して、client credentials flow(Machine to Machine)を利用したアクセスのためのポリシーを登録します。
?principal
は、ポリシーテンプレートのプレースホルダで、実際のポリシーの登録時に、具体的な値に置き換えられます。
今回は、Cognitoで発行したM2M用のクライアントのclientId
を設定しました。
ポリシーテンプレートに関連づけられたポリシーは、ポリシーテンプレート側の変更を反映することが可能です。
下記のテンプレートから分かるように、今回M2MアクセスへはReadOnlyアクセスを許可するように設定しています。
- template
permit (
principal == ?principal,
action in [FastapiApp::Action::"get /tenants/{tenant_id}/items"],
resource
);
下記がテンプレートから、実際に作成されたポリシーになります。
- policy
permit (
principal == FastapiApp::Client::"6tpsbt0o9hbjrso9at1m59g74j",
action in [FastapiApp::Action::"get /tenants/{tenant_id}/items"],
resource
);
コード: https://github.com/seiichi1101/avp-sample-with-fastapi/blob/main/terraform/cedar/template1.cedar
Pythonのコード(アクセス制御部分)
実際のアクセス制御を行うFastAPIのコードは、以下のように実装します。
ポイントは、AVPへアクセス判定をリクエストする際に、entities
にユーザーとテナントの親子関係の情報を付与している点です。
これは、AVPないしCedar自体には、ユーザーやテナントなどのエンティティ自体の情報を管理する機能を持たないため、別途Entityを管理する仕組み(以下エンティティストアと呼び)を用意し、そこから必要な情報を取得してAVPに渡す必要があることを意味しています。
今回は、Cognito User Poolのユーザーとグループ情報を利用しているため、Cognitoの一部がエンティティストアとして機能していますが、特に制限はありません。
ID基盤が持つアクセス制御に縛られずに、独自のエンティティストアを用意して、必要な情報をAVPに渡すといった設計も考えられます。
# action_idはAPIのリクエストパスに基づいて動的に設定
action_id = f"{request.method.lower()} {request.scope["route"].path}"
# pathに含まれるテナントIDを取得
path_tenant_id = request.path_params.get("tenant_id")
# 検証済みのtoken payloadからユーザーのアクセスかクライアントのアクセスかを判定
is_user_access = payload.get("username")
if is_user_access:
# ユーザーのアクセス制御
avp_input = {
"policyStoreId": POLICY_STORE_ID,
"principal": {
"entityType": "FastapiApp::User",
"entityId": payload.get("sub"),
},
"action": {
"actionType": "FastapiApp::Action",
"actionId": action_id
},
"resource": {
"entityType": "FastapiApp::Application",
"entityId": "Any"
},
"entities": { # entitiesにユーザーとテナントの親子関係の情報を付与してAVPに渡す
"entityList": [
{
"identifier": {
"entityType": "FastapiApp::User",
"entityId": payload.get("sub")
},
"parents": list(map(
lambda tenant_id: {
"entityType": "FastapiApp::Tenant",
"entityId": tenant_id
},
payload.get("cognito:groups", [])
))
},
{
"identifier": {
"entityType": "FastapiApp::Application",
"entityId": "Any" # 今回Resourceは明示しないのでAnyに固定
},
"parents": [
{
"entityType": "FastapiApp::Tenant",
"entityId": path_tenant_id # リソースの親にはリクエストパスから取得したテナントIDを設定
}] if path_tenant_id else []
}
]
}
}
avp_result = avp_client.is_authorized(**avp_input) # AVPにアクセス制御のリクエストを送信
if avp_result.get("decision") != "ALLOW":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized",
headers={"WWW-Authenticate": "Bearer"}
)
return User(
sub=payload.get("sub"),
tenants=payload.get("cognito:groups", [])
)
else:
# クライアントのアクセス制御
avp_input = {
"policyStoreId": POLICY_STORE_ID,
"principal": {
"entityType": "FastapiApp::Client",
"entityId": payload.get("client_id"),
},
"action": {
"actionType": "FastapiApp::Action",
"actionId": action_id
},
"resource": {
"entityType": "FastapiApp::Application",
"entityId": "Any"
}
}
avp_result = avp_client.is_authorized(**avp_input)
if avp_result.get("decision") != "ALLOW":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized",
headers={"WWW-Authenticate": "Bearer"}
)
return Client(
id=payload.get("client_id")
)
...
コード: https://github.com/seiichi1101/avp-sample-with-fastapi/blob/main/app/auth.py
Cognito User Poolの設定
Cognito User Poolのグループ機能を利用して、ユーザーとグループの作成を行い、ユーザーをグループに所属させます。
アクセス制御が出来るか確認する
ここではユースケースを毎に、テナントやユーザー属性に基づいたアクセス制御が出来るか確認します。
classmethod
に所属するユーザーのアクセス
特定のテナントGET /items
では、所属するテナントclassmethod
のアイテム一覧を取得できること
curl -X 'GET' \
'https://example.execute-api.ap-northeast-1.amazonaws.com/items' \
-H 'accept: application/json' \
-H 'Authorization: Bearer <token>'
[
{
"id": 1,
"tenant_id": "classmethod"
},
{
"id": 2,
"tenant_id": "classmethod"
},
{
"id": 3,
"tenant_id": "classmethod"
}
]
GET /tenants/classmethod/items
でアイテム一覧を取得できること
curl -X 'GET' \
'https://example.execute-api.ap-northeast-1.amazonaws.com/tenants/classmethod/items' \
-H 'accept: application/json' \
-H 'Authorization: Bearer <token>'
[
{
"id": 1,
"tenant_id": "classmethod"
},
{
"id": 2,
"tenant_id": "classmethod"
},
{
"id": 3,
"tenant_id": "classmethod"
}
]
GET /tenants/annotation/items
へはアクセスできないこと
curl -X 'GET' \
'https://example.execute-api.ap-northeast-1.amazonaws.com/tenants/annotation/items' \
-H 'accept: application/json' \
-H 'Authorization: Bearer <token>'
{
"detail": "Not authorized"
}
POST /tenants/classmethod/items
にアイテムを追加できること
curl -X 'POST' \
'https://example.execute-api.ap-northeast-1.amazonaws.com/tenants/classmethod/items' \
-H 'accept: application/json' \
-H 'Authorization: Bearer <token>' \
-d ''
{
"id": 7,
"tenant_id": "classmethod"
}
M2Mクライアントのアクセス制御
GET /items
では全てのアイテム一覧を取得できること
curl -X 'GET' \
'https://example.execute-api.ap-northeast-1.amazonaws.com/items' \
-H 'accept: application/json' \
-H 'Authorization: Bearer <token>'
[
{
"id": 1,
"tenant_id": "classmethod"
},
{
"id": 2,
"tenant_id": "classmethod"
},
{
"id": 3,
"tenant_id": "classmethod"
},
{
"id": 4,
"tenant_id": "annotation"
},
{
"id": 5,
"tenant_id": "annotation"
},
{
"id": 6,
"tenant_id": "annotation"
},
{
"id": 7,
"tenant_id": "classmethod"
}
]
GET /tenants/classmethod/items
でアイテム一覧を取得できること
curl -X 'GET' \
'https://example.execute-api.ap-northeast-1.amazonaws.com/tenants/classmethod/items' \
-H 'accept: application/json' \
-H 'Authorization: Bearer <token>'
[
{
"id": 1,
"tenant_id": "classmethod"
},
{
"id": 2,
"tenant_id": "classmethod"
},
{
"id": 3,
"tenant_id": "classmethod"
},
{
"id": 7,
"tenant_id": "classmethod"
}
]
POST /tenants/classmethod/items
へはアクセスできないこと
curl -X 'POST' \
'https://example.execute-api.ap-northeast-1.amazonaws.com/tenants/classmethod/items' \
-H 'accept: application/json' \
-H 'Authorization: Bearer <token>' \
-d ''
{
"detail": "Not authorized"
}
所感
実際にAVPとCedarを使ってみて感じた点をいくつか挙げます。
デザイン力が問われる
Cedarは非常に柔軟なポリシー言語であり、複雑なビジネスロジックや条件付きのアクセス制御も記述可能です。
しかし、その自由度の高さゆえに、デザイン力が求められます。
例えば、今回はAVPのデモを参考に、ポリシーのActionsに静的な値(パス)を設定し、Resourcesは特に設定しない形で実装しました。
こうすることで、ポリシーの設計が比較的シンプルになり、理解しやすくなりました。
しかし、実際にはリソースはResourcesに、アクションはActionsに明示的に分けて設計する方が一般的かもしれません。
制限対象のリソースをActionsに設定するか、Resourcesに設定するかは設計上の悩みどころです。
また、今回利用しませんでしたが、下記の様により抽象的なpolicyを書くことも出来ます。
permit (
principal,
action in
[FastapiApp::Action::"get /tenants/{tenant_id}/items",
FastapiApp::Action::"post /tenants/{tenant_id}/items"],
resource
)
when { principal in resource };
こちらは、許可するActionはpolicyで制約されていますが、あとは「principal in resource」という条件を満たす限り、どのようなリソースに対しても適用可能となります。
つまり、principalとresourceの親が同じであれば、どのようなリソースに対してもアクセスを許可することができます。
そのため、この場合、テナント所属確認ロジックはCedar上で評価されないので、クライアント側で明示的に実装する必要があります。
あくまで、ここでは「親が同じであれば、下記のActionに対しては許可する」というポリシーを示しています。
いずれにせよ、アクセス制御部分は一度実装してしまうと、なかなか変更しずらい部分だと思うので、拡張性を持たせる形で慎重に設計する必要があり、とてもデザイン力が必要だと感じました。
下記に、ユースケース別のサンプルがあるので、検討する際は目を通しておくと良さそうです。
Entity Storeの考慮も必要
AVPはポリシーの管理と、評価エンジンを提供しますが、実際のエンティティ(ユーザーやリソース)に関連する情報は、外部ストアが必要です。
今回の様に認証基盤の機能を利用して、疑似的にエンティティストアとすることも可能ですが、アクセス制御をより柔軟に行うためには、独自のエンティティストアを用意するのが良さそうです。
特に、マイクロサービス環境化での利用を想定する場合、各サービスと比べ、アクセス制御部分は1つ上のレイヤーになるかと思います。
システム全体に大きく影響するため、高可用で柔軟な連携ができる仕組みを検討する必要がありそうです。
パッと思いつくのは、DynamoDBやMomentのような低レイテンシで高アクセスをさばけるようなデータストアですが、負荷試験なども含めて十分な検証が必要そうです。
ポリシーテンプレートの制約
ポリシーテンプレートでは、principal
とresource
のみがプレースホルダ化でき、actionはできません。
また、テンプレートポリシーのwhen
句で?principal
が使えないなど、テンプレートには一定の制約があります。
下記の様なテンプレートを作成したかったのですが、現状では実現できません。
ここら辺は、Cedarの設計理念もあると思うので、もう少しCedar自体の理解を深める必要がありそうです。
permit (
principal in ?principal,
action in
[FastapiApp::Action::"get /tenants/{tenant_id}/items",
FastapiApp::Action::"post /tenants/{tenant_id}/items"],
resource
)
when
{
principal in ?principal &&
resource in ?principal
};
https://docs.cedarpolicy.com/policies/templates.html
運用面での課題感
ポリシーの作成は、Cedar言語に慣れていないと難しい部分があります。
たとえば、AVPのコンソール画面から、非エンジニアの方に、直接ポリシー(アクセス制御)を追加・編集してもらうといったことは難しいです。
運用のやり方次第ではありますが、エンジニア以外の人がするユースケースがあるのであれば、何かしらのUIやツールでラップしてあげる必要がありそうです。
その他
IsAuthorizedWithToken
の利用は微妙かも
今回は利用しませんでしたが、AVPにIdPの設定を追加することで、AVPのIsAuthorizedWithTokenを利用した際に、「トークンの検証」と「アクセス制御の判定」の両方をAVPがやってくれます。
イントロスペクションエンドポイント的に使えそうなのですが、失敗した際のレスポンスから詳細な情報が取得できないため、アプリ側でのクライアントへのレスポンス生成する際に困ります。
また、どこまでスケールするかは不明です。
- AVPによるカスタムオーソライザー自動作成はチュートリアルとしては使える
AWS Verified Permissionsで自動セットアップしてくれる機能があり、API GatewayとIdPの情報から、Policy Storeの作成、Policyの作成、API Gatewayのカスタムオーソライザー用のLambda(Token検証とACL含む)の作成などを自動で行ってくれます。
ただし、実際のプロジェクトでは、リソースやコードの管理はそれぞれのツールを使って行うことになるし、認可のコードは失敗した際のレスポンスもいろいろ書くことあるので、利用は現実的ではないかもしれません。
ただ、AVPの使い方を理解するにはとても良いです。
https://www.youtube.com/watch?v=OBrSrzfuWhQ
まとめ
Cedar自体が汎用的なポリシー言語であるため、自由度が高いですが、その反面デザイン力を求められます。
今回の利用ケースでは、Resourceを利用していなかったため比較的理解しやすいですが、真面目にResourceを利用していくと、ポリシーの設計の難易度が格段に上がりそうです。
アクセスコントロールについて深く検討していなかったユーザーにとっては、どこから手をつけていいのかわからないのかもしれません。
そんな方は、是非クラスメソッドに相談してみてください!
また、今後のアップデートやエコシステムの発展にも注目したいところです。