マイクロサービスアーキテクチャ(以下MS)で開発をしていて、認可をどこで実行するか悩んだ末にとりあえず答えみたいなものに行き着いた気がしたのでメモしておきます。
悩みごと
WebフレームワークにありがちなMVCアーキテクチャ(?)な場合、大体ユースケースは以下のような手続きになると思います。
- コントローラーでリクエストを受け取る
- 操作する対象のリソースをロード
- ロードしたリソースに対して認可を実行
- 権限を有する場合に限って処理を続行
- レスポンスを返す
Webフレームワークでも「このユースケースをバッチからも実行したい」という要求はありがちです。
これに対応するために何とか工夫して認可を無視できるようにしたり、はたまた似たような処理を別途実装したりする事例を何度か見てきました。
さて、これがMSになるとどうなるでしょうか。
特定のMSから呼び出されるための権限チェックがないAPIを準備したり、工夫のコストが随分高くなったのを感じました。
どうにかして認可の詳細を知らぬまま認可のコントロールだけをクライアント側に移動させたかったのですが、なかなか良い方法が思いつかずにいました。
難しいポイント
認可を行うにはIDを元にリソースをロードした後にそのプロパティを元に認可を行う場合が大半で、ユースケースやMSの外側に持ち出す事が難しいと感じました。
例えば記事IDPostId
が分かっていても、その著者AuthorId
はPost
リソースをロードして初めて分かるためです。
def put(id: PostId, title: String): F[Unit] =
repository.update(id) { maybePost =>
maybePost
.pure[F]
.raiseEmpty[F](new NotFound("Post not found."))
// ↓ この部分の動作だけを外部からコントロールできるようにしたい
.ensure(new Forbidden("You are not the author of this post.")) { post =>
// ここまで来ないと著者が分からない
post.authorId == user.id
}
.map(_.copy(title = title))
}
認可をクライアントにさせても良いのですが、そうなるとクライアントがどう認可を行うかという知識を有していなければならず、好ましくないと感じました。
とりあえず認可を無視できる方法を準備した
正直良い方法が思い浮かばなかったので、スーパーユーザー的な認証情報を渡し認可を突破する方法で難を凌ぎました。
大体のユースケースで不都合はなかったのですが「誰がアクセスしてきているのか」という情報が落ちてしまうため、とても良かったという結果にはなりませんでした。
より良い解決策
ほぼ完全にAWSのIAMの影響ですが、リソースのIDに認可の実行に必要なデータを含めるという手法です。
例えば、記事Post
のリソースIDにはPostId
とAuthorId
を含めたauthors/${AuthorId}/posts/${PostId}
のような形式にし、それに対する権限があるかどうか確認します。
この方法であればリソースIDだけで記事Post
の所有者かどうかが確認する事が可能になりますよね。
つまりはユースケース内部から認可を外し、ブラックボックスな認可をクライアント側から呼び出す事で認可の有無をコントロールできるようになります。
IAM Policy風に定義するならばPrincipal: user-1
にauthors/user-1/posts/*
に対するAction: "Post:*"
を許可するようなデータ構造を認可MSに持たせておけば良いのです。
IAMのPolicyにも複雑な条件が記述できる事と同様に、リソースID単体で全てのケースに対応できる訳ではないと考えていますが、Policyを高機能化させる事で様々な条件に対応が可能なはずです。