SpiceDBで認可制御するLambda Authorizerを作ってみた
はじめに
OSSの認可制御基盤であるSpiceDBを使ってAWS API Gatewayで公開したAPIの認可制御をおこなうためのLambda Authorizerをつくりました.
ソースコードはこちら.
SpiceDBの元になった論文の解説や認可制御の方法は過去の記事に記載しています.
システム構成
システム構成は主に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の処理を抜粋したものが以下です.処理としては,
- AuthorizationヘッダからJWTを抜き出す
- SpiceDBの操作を行うAuthorizerインスタンスを生成
- JWTからユーザ名を,リクエストパスから対象リソースIDを,リクエストメソッドからチェックするパーミッション情報を生成
- AuthorizerインスタンスからSpiceDBにパーミッション検証要求を送信
- 検証結果を元に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