【レポート】<認証の標準的な方法は分かった。では認可はどう管理するんだい?> – Developers.IO FUKUOKA/TOKYO 2019 #cmdevio

2019年11月1日に、クラスメソッド主催の技術カンファレンス「Developers.IO 2019 TOKYO」が開催されました。本記事では都元ダイスケによるセッション「認証の標準的な方法は分かった。では認可はどう管理するんだい?」をレポートします。

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

旬の生魚おじさん、都元です。11月ですよ! Developers.IO イベントが今年もやってまいりましたよ。サンマもやってきていますよ! もう召し上がりましたでしょうか? 今年のサンマは高いですが…、強く生きようと思います。

さて、去る 10/26(土) 福岡にて、また 11/1(金) も東にて Developers.IO 2019 イベントを開催しました。これらのイベントで「認証の標準的な方法は分かった。では認可はどう管理するんだい?」と題しましてお話させていただきました。

本エントリーは、そのセッションレポートとなります。

スライド

個人的な話ですが、ここ数年の登壇では OpenID Connect の話をさせていただくことが多くなっております。そこでよくある質問として「認証についてはよく理解できました。では、認可って何かうまい仕組みで管理できないもんでしょうか?」というものがあります。

と、いうわけでその質問をそのままタイトルにしてみたのが本セッションです。 というわけで順次内容をご紹介していきます。

Part 0

この手の話をする場合に、まず毎度出すスライドがコレです。

もう見飽きた方もいるかもしれませんがw 日常生活では、どちらも「カギ」と呼んでしまいがちですが、正確には右側の方は「ジョウ」と呼びます。アクセス制御を考えるにあたっては「何がどんな key を持っていて、何にどんな lock が掛かっているのか」を整理して考えるとわかりやすいと思います。

Part 1

次に、Java のフレームワーク Spring Security のアーキテクチャから着想を得て、アクセス制御を 4 つに分類してみました。

アクセス制御の分類には、次の 2 軸があります。

  • アクセス制御を、リクエストに基づいた処理を行う事前に行うか、事後に行うか
  • 判定の結果 NG だと判断した場合にアクセスを拒否 (積極的)するのか、リクエストやレスポンス等を加工 (フィルタリング等、消極的) するのか

この 4 象限の中で、最も一般的なアクセス制御が「積極的な事前アクセス制御」ですね。 そして他の 3 つのパターンも存在すると言えばするのですが、できる限りこの左上のパターンに寄せていくと物事がシンプルになると私は考えています。

本当は全種類それぞれご紹介もしたかったのですが、時間の都合もあり、今回は左上の「積極的な事前アクセス制御」だけに重点を絞ってお話をしました。右下のヤツも、後ほどほんの少しだけ触れます。

ちなみにこの場での「事前・事後・積極・消極」という用語は私独自によるものです。一般用語ではないことにご留意ください。
また「加工 (消極的なアクセス制御) はアクセス制御なのか?」というと専門的にはよくわかりません。厳密な定義をお求めの場合は他をあたってください。

Part 2

さて。認可にはデファクトスタンダードが無い、と言いましたが。半分嘘です。 デファクトかどうかは各自のご判断に任せますが、ISO スタンダードはあります。

アクセス元 (Initiator) があって、そこから来たリクエストを AEF がポリシー施行します。そして、ADF がポリシー定義に従って「判定結果」を返します。判定に問題がなければ、アクセス先 (Target) にリクエストが到達します。わかりやすいですね。っていうか普通こうなりますね。

ちなみに、ちゃんと読んでみたいとは思ったのですが、読むには課金しなきゃいけないようなので断念しています。誰か買ってください。

さて、ここで注目したいのは ADF (Access control Decision Function) です。 この関数 (function) は、リクエストの文脈情報 (ADI = Access control Decision Information) に基づいて判定結果 (boolean) を返す関数です。

では ADI とはどのような情報でしょうか? よくあるのは、

  • 誰がリクエストしてきたのか? (認証)
  • いつリクエストしてきたのか? (時刻)
  • どのノードからリクエストしてきたのか? (IPアドレス)
  • 何にリクエストしてきたのか? (対象リソース)
  • どの機能をリクエストしてきたのか? (操作)
  • どのようなパラメータでリクエストしてきたのか? (操作)

辺りでしょうか。しかし、お客様が望む「要件」には何が来るかわかりません。 意地悪に考えればいくらでも無茶苦茶な要求ができるわけです。言い換えれば、ADI はあらゆる情報を含む可能性があります。

本セッションではこの ADI とそれを処理する ADF 実装を整理します。

Part 3

ここからが本番です。パート割的には最後のパートですが、時間配分的にまだ半分に到達した頃ですw

アクセス制御を考えるときは、まず次の要素があると考えます。

  • 「操作するヒト」 → ユーザー (subject, initiator, 人間やプロセス)
  • 「操作されるモノ」 → リソース (object, target, システムやファイル)
  • 「何かの操作」 → アクション (メソッド的な呼び出す機能の名前、動詞) とパラメータ (機能呼び出し時の引数)

ユーザー (Initiator) とリソース (Target) は ISO 10181-3 にも出てきた要素ですが、 その他に「アクション」というものをきちんと定義するのがポイントです。

とあるアプリケーションに対する、非常によく似た (けど微妙に違う) 2 つの HTTP リクエストがあると考えてみてください。この 2 つのリクエストは

  • 同じアクションを呼び出していて、パラメータが違うだけ
  • (非常に似ているが、実は) そもそも違うアクションを呼び出している

どちらだか、客観的に整理できるでしょうか? それを整理できるようにするのが名前です。

脱線

はい、ここからはセッション本編でも話さなかった雑談を少し。

昔やった LT のネタなんですが。名前が無い世界においては、複数の要素の間に境界線がありません。ここで誰かが、ある色に「赤」という名前を付けると、すべての色が「赤い色」と「赤くない色」に分断されます。(ただし、ちゃんと共有しないと境界線は人それぞれですね、っていうのがこの LT の主旨なんですが)

そもそもアクションに名前を付けないままアプリケーションの設計を行うと、個々のリクエストを分類整理できなくなってしまうのです。

というわけで、ここではアクションの名前を決めてきちんと分類できるように定義しましょう。閑話休題。

アクション名およびユーザー権限という ADI

さて、アクションにきちんと名前をつけて、客観的な定義もしました。つまり、アプリケーションはリクエストを受けた時、その内容からアクション名を 1 つに決められる、ということです。

このとき adi.action によってそのアクション名を参照できる、とします。 さらにユーザーが持っている権限は配列型で adi.sub.authorities によって参照できる、とします。

すると。ADF は例えばこのように定義できると思います。普通ですね?

いや、普通って大事なんですよ。

パラメータを ADI にしていいか?

結論から言えば、避けたほうが無難です。

例えば UpdateEmp というアクションでは、従業員の名前も給与も更新できるとします。 そして、こんなアクセス制御をしたい。

誰が \ 何をする カガさんの名前を書き換える カガさんの給与を書き換える
カガさんが ×
エザキさんが
タカダさんが ×

何を書き換えようとしたのか? はアクション名ではなくパラメータに属する、という整理をしたのがこの事例です。

私は、このようなアクセス制御をしたい場合は、このような整理をするのではなく、そもそもアクションを分けるべきだと考えています。

例えば、UpdateEmp では従業員の名前しか更新できない。UpdateEmpByAdmin では従業員の給与しか更新できない。とします。

そうすれば、ADF はパラメータを見ること無く判定結果を出せるようになります。

誰が \ 何をする カガさんに対して UpdateEmp カガさんに対して UpdateEmpByAdmin
カガさんが ×
エザキさんが
タカダさんが ×

ADF が個別のアクションのパラメータ構造に依存すると、それはそれは複雑なしくみが出来上がってしまいます。アクションの「名前」という単なる文字列に依存するくらいにとどめられると、シンプルな仕組みが維持できるでしょう。

事後消極アクセス制御は、なぜ利用しないのか?

消極的アクセス制御というのは、具体的に言えば「見ちゃいけない情報を勝手に削って返す」ことです。え、これ便利じゃない? そんなふうに考えていた時期が俺にもありました。

よし分かった。更新系のアクションにおいて「フル権限がある時は A, B, C すべての処理を行うが、実行しちゃいけない処理がある場合は勝手に削って、例えば A, C の処理だけを実行して OK を返す」…。これも消極的アクセス制御の一種です。

なんとなく怖くなってきました?

求める処理内容やレスポンス形式が異なる、ということは、そもそもユースケースが異なると考えた方が良さそうだ、と私は考えています。

UML における「ユースケース図」というものがあります。棒人間と、それが実行するアクションを線でむすぶだけのシンプルな体系の図です。正直なところ、私がこれを初めて見たときは「バカにしてんのか?」と思ったくらいシンプルです。

いや、これが良く出来てるんすよ…。この状況をユースケース図で整理してみるとおそらく、「フルで実行する操作」と「自動的に何かを削って実行する操作」は別のアクターから矢印が出て、別のアクションにつながるように記述できるのではないでしょうか。 うん、大抵そうなるはずです[要出典]

というわけでもし消極的なアクセス制御をしたくなったら、ユースケース図に従って「別のアクション」として整理することによって、アクションレベルでのアクセス制御に寄せて統一できるか、検討してみるべきです。

リソース毎のアクセス制御 (オーナー権限編)

さて、ここまでは adi.actionadi.sub.authorities という 2 つの ADI を使ってアクセス制御を行ってきました。 しかしこのタイプのアクセス制御は「UpdateEmp できる権限を持っていれば、全てのリソースに対して UpdateEmp できる」ということになってしまいます。

そうではなく「自分のリソースの UpdateEmp はできるが、他人のリソースの UpdateEmp はできない」みたいなことは、よくある要件です。

そこで登場するのがオーナーという ADI です。オーナーはリソースが持つプロパティであり、adi.obj.owner でアクセスできるとしましょう。 また、ユーザー名は ad.sub.name でアクセスできるとすれば、これらの値が一致した時にロックが外れる ADF は簡単に書けますね。

リソース毎のアクセス制御 (リソースポリシー編)

次に。「重要度の低いリソースは操作できるが、大事なリソースは操作できない」というのも、それなりにありうる要件です。先程はリソースにオーナーという考え方を導入しましたが、今回はオーナーシップが関係しないアクセス制御なので、先程のようにはいきません。

ここで導入するのが ACL (アクセス制御リスト) という考え方です。 ユーザーがユーザー毎個別に authorities というプロパティを持っているのと同様に、リソースもリソース毎個別に acl というプロパティを持っている、と考えます。

そしてこのプロパティには、「どんな人が何をしていいか」が列挙してある状態を作ります。

という辺りでお時間です。

まとめ

冒頭で「カギ」と「ジョウ」の話をしました。 これまで色々話してきた ADI が「カギ」に相当します。そして ADF が「ジョウ」です。

「ADI (カギ) を持っているのはユーザー」というのは一般的なイメージだと思います。 が、本セッションではアクション自体も「アクション名という ADI (カギ)」であること、そして「リソースもまた ACL という ADI (カギ) を持っている」ことを示しました。

そしてアクセス制御は 4 つに分類して考えますが、なるべき「事前積極的アクセス制御」に倒すことを考えましょう、という方針を示しました。

また、ADI には無数のバリエーションがありえますが、下記を軸にして整理することをオススメしました。

  • リクエストしたアクション名 adi.action
  • ユーザー名 adi.sub.name
  • ユーザーが保持する権限 adi.sub.authorities
  • リソースのオーナー adi.obj.owner
  • リソースのアクセス制御リスト adi.obj.acl

というわけで、セッションを聞いていただいたみなさま、レポートを最後まで読んでいただいたみなさま、ありがとうございました。

さいごに

我々のチームでは仲間を募集しております!