🦥

SpiceDBで認可制御するLambda Authorizerを作ってみた

2022/10/21に公開

はじめに

OSSの認可制御基盤であるSpiceDBを使ってAWS API Gatewayで公開したAPIの認可制御をおこなうためのLambda Authorizerをつくりました.
ソースコードはこちら.
https://github.com/manaty226/lambda-authorizer-spicedb

SpiceDBの元になった論文の解説や認可制御の方法は過去の記事に記載しています.
https://zenn.dev/manaty226/articles/7677ce3a08ddeb
https://zenn.dev/manaty226/articles/71bee4c1a02761

システム構成

システム構成は主にAPI GatewayとAuthorizerとなるLambda,それにSpiceDBをホストするECSで構成されます.InitializerのLambdaはSpiceDBの初期化(スキーマの登録と認可)を簡単に行うために作っています.また,プライベートSubnetに配置したECSやLambdaが外部や他のAWSサービスと通信するためにPublicサブネットにNAT Gatewayを置いてInternet Gatewayを介して接続するパターンで構成しています.

LambdaとSpiceDBを利用した認可制御のフロー

API GatewayにアタッチしたLambda AuthorizerからSpiceDBを介して認可制御するシーケンスを下に示します. ClientはリクエストヘッダにJWTをBearerトークンとして付与してAPI Gatewayにアクセスします.API GatewayはアタッチされたLambda AuthorizerにJWTやリクエスト情報を伝搬し,Lambda Authorizerはそれらリクエスト情報を読み取ってSpiceDBにパーミッションチェックの要求を投げます.SpiceDBのチェック応答から認可OK/NGを判断し,API GatewayからClientへの応答を決定します.

Lambda Authorizerの処理

認可制御を行うLambdaの処理を抜粋したものが以下です.処理としては,

  1. AuthorizationヘッダからJWTを抜き出す
  2. SpiceDBの操作を行うAuthorizerインスタンスを生成
  3. JWTからユーザ名を,リクエストパスから対象リソースIDを,リクエストメソッドからチェックするパーミッション情報を生成
  4. AuthorizerインスタンスからSpiceDBにパーミッション検証要求を送信
  5. 検証結果を元にLambda AuthorizerからAPI Gatewayに応答
def lambda_handler(event, context):
    # extract token from header
    token = Token.parse(event["headers"]["authorization"].split(" ")[1])  
    ...
    # create authorizer
    ...
    authorizer = Authorizer(host, port, "test", bytes(cert, "utf-8"))
    ...
    # create Authorization Object
    user = AuthzObject("user", name)
    resource = AuthzObject("blog", resource_id)
    permission = "read" if method == "GET" else "write"
    
    # check permission via SpiceDB
    isAllowed = authorizer.check_permission(resource, user, permission)
    ...
    

APIにアクセスしてみる

リソースのデプロイ

ソースコードのdeploymentsフォルダにTerraformで記述したリソース一式があるのでterraform applyで必要な環境が構築できます.ただし,lambda/layer配下のauthzedライブラリはご自身でインストールしてLambda Layerが作成できる状態にしてください.

SpiceDBの初期化

リソースがデプロイされたらspicedb_initializerという名前のLambdaをテスト実行してSpiceDBを初期化します.ここでは,過去の記事と同様のブログリソースとユーザで構成される下のスキーマが登録されます.

definition user{}

definition blog {
	relation reader: user | user:*
	relation writer: user

	permission write = writer
	permission read = reader + writer
}

また,このスキーマに対して,Taroというユーザのwriterパーミッション(書き込みと読み取りが可能)をID=1のブログに与えています.

JWTの準備

API Gatewayへアクセスする際にヘッダに付与するベアラトークンはJWT.ioで作成します.とりあえずnameクレームがあれば良いので.以下のように作成.

API Gatewayにアクセス

まず,ID=1のブログ内容を取得するため,blogs/1のパスにアクセスします.この操作は先ほどInitializerのLambdaでTaroに認可権限を与えたのでブログの内容が応答としてかえってきます.

$ curl https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/blogs/1
-H 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRhcm8iLCJpYXQiOjE1MTYyMzkwMjJ9.mIFsjmgmsMNA6gVk3q6iuNJTAFvVmlSVsjVh5MQ4cIg'

{
 "title": "test blog",
 "content": "This is test blog"
 }

次に.blogs/2のパスにアクセスしてみます.このブログIDのリソースにはパーミッションを与えていないため,
User is not authorized to access this resourceで権限がないよと怒られます.

curl https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/blogs/2 -H 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRhcm8iLCJpYXQiOjE1MTYyMzkwMjJ9.mIFsjmgmsMNA6gVk3q6iuNJTAFvVmlSVsjVh5MQ4cIg'
{"Message":"User is not authorized to access this resource"}

おわりに

Lambda Authorizerを介してSpice DBでAPI Gatewayの認可制御を行うことができました.
本質的な部分よりVPC LambdaからECSへの接続経路をどうするか(素直にALBを使うかCloudMapを使うか,あるいは...)や,AuthzedライブラリのLayer作成,TerraformとOpenAPIを組み合わせたときのAuthorizerのデプロイ方法(TerraformとOpenAPI両方で定義するとパスにアタッチされない)みたいなところにハマって時間を使った気がします....逆にいうとLambda AuthorizerとSpiceDBを使って認可制御するのは結構手軽にできるので,Cognitoユーザグループやアクセストークンのスコープ制御より細かい認可制御をSpiceDBで行うという選択肢はありかもしれません.

Discussion