アプリケーションにおける権限設計の課題

f:id:kenev:20200113114803p:plain

日々権限設計で頭を抱えてます。この苦悩が終わることは無いと思ってますが、新しい課題にぶつかっていくうちに最初のころの課題を忘れていきそうなので、現時点での自分の中でぐちゃぐちゃになっている情報をまとめようと思い、記事にしました。

所々で「メリット」「デメリット」に関連する情報がありますが、そのときそのときには色々と感じることがあっても、いざ記事にまとめるときに思い出せないものが多々ありました。フィードバックや自分の経験を思い出しながら随時更新する予定です。

TL;DR(長すぎて読みたくない)

  • 権限設計は難しく、ベストプラクティスが広まっていない
  • ネット上に散らばっている情報をかき集めながらBetterなアプローチを追求し続けないといけない
  • 権限設計のノウハウがもっと公に共有されてほしい

本当に長い記事なので目次に気になるトピックがある方だけ続きを読んでください。ところどころ感想文になっていたりする点に関してはあらかじめお詫び申し上げます。

この記事へのフィードバックだったり、僕の継続的な苦悩から生まれる新たな知識をアップデートしていくことで、今後改善させるつもりではあります!

想定する読者や前提知識

  • API設計をしたことがある
  • 権限をどうやって実装するべきか苦しんだことがある
  • ソフトウェアアーキテクチャについて意識している

この記事での権限とは

この記事における権限とは、「誰が(Principal)」、「何に(Resource)」、「何を(Action)」して良い(Allow)のか、あるいはしてはいけない(Deny)のかを定義するものに関して述べています。

例えばInstagramを例にとると、

  • フォローしている人の投稿を参照することができる
  • 自分の投稿を作成することができる(他人の投稿は作成できない)
  • 自分の投稿を削除することができる(他人の投稿は削除できない)
    • 自分の投稿のメニューには「削除」メニューがある(他人の投稿には無い)

のようなものです(かなりシンプルな例を出しました)。

アプリケーションを作る上でほとんどの場合こういう観点の権限が何かしら登場すると思います。

では、この「権限」をどうやって設計・実装するべきなのか?ということがこの記事のメイントピックです。

間違った理解で書いている内容もあるかと思いますが、何かしらの方法でご指摘いただけると幸いです。(その分また一つ権限レベルが上がる気がします)

権限の種類

まず、権限モデルには様々なものがあります。その中でもACL, RBAC, ABACについて軽く紹介したいと思います。

dzone.com

上の記事がわかりやすく説明しているのですが要点だけ言うと以下のとおりです。

ACL(Access Control List)

f:id:kenev:20200106214744p:plain:h100

ACLは「ある対象(よく例に出てくるのはファイルだったりディレクトリだったり)」に対して利用者の何らかのアクセス権限をリスト化したものです。

「勉強会で出席者名簿に名前があれば参加できる」

というように、一覧に入っていればOKというようなシンプルな制御です。

RBAC(Role-Based Access Control)

f:id:kenev:20200106214525p:plain:h150

RBACは、「ある対象(例えばユーザー)」に「ロール(一般ユーザー・管理者など)」という概念が割当てられるようにしたものです。このロールに対して、「何ができるか」というのが付与されるようになります。

「勉強会の一般参加者はGUESTカードを渡され、運営側の人はSTAFFカードが渡される。STAFFカードを持っている人は特定の部屋に入ることができる。」

というように特定のロールに、特定の行動許可が付与されるようなイメージです。

ABAC(Attribute-Based Access Control)

f:id:kenev:20200106214655p:plain:w150

ABACはさらにもう一歩先にいきます。ABACのAがAttribute(属性)であるように、「ある対象の属性」も考慮して「何ができるか」を決めることができます。

「AさんもBさんも勉強会に参加しました。Aさんは20歳なので懇親会でお酒が飲めますが、Bさんは18歳なので懇親会でお酒が飲めません。」

というように、「ある対象(Aさん、Bさん)」の「属性(年齢)」に着目して「何ができるか(お酒が飲めるか)」を決めることができます。

どの権限モデルを採用するべきか

3つの権限モデルを紹介しましたが、他にも色々とあります。どの権限モデルを採用するべきなんでしょう?あるあるですがそれぞれのメリット・デメリットがあるので「It depends」です。上で紹介した中ではABACが最も柔軟ですが、その分実装するのも大変になります。要件がロールだけで完結するアプリケーションであれば、RBACで作る方がシンプルになると思います。ただし、下のようなケースが出てくるとだいたいの場合ABACになっちゃうのかなと思います:

  • 同じグループだったら○○できる
  • 友達だったら○○できる
  • ○○歳以上なら○○できる
  • YYYY年〜ZZZZ年の間に加入した人なら○○できる
  • などなど

要件を見極めながら権限モデルを決める必要があります。

権限を適用する場面

権限モデルについて触れたところで、次はどこで権限を適用するのか、という点について考えます。これには次の3つの観点が重要なのかなと思っています。

  • 機能的な観点
  • 対象範囲の観点
  • 対象項目の観点

タイミング的にはAPIであれば「リクエスト到着時」、「データ取得時」、「データ返却時」に分類できると思っています。下図のそれぞれのポイントになります。

f:id:kenev:20200107183716p:plain

参考までに、Clean Architecture的に言うと下図のポイントに該当します。

f:id:kenev:20200106231020p:plain

それぞれをもう少し詳しくみていきます。

機能的な観点

機能的な観点というのは「そもそも対象となる機能が使えるのか」というかなり広い範囲の観点です。APIであれば、リクエストしてきた人が「何者かわかれば」判断を下すことができます。「一般ユーザーだったら管理者画面を閲覧できない」というようなケースがこれにあたります。

f:id:kenev:20200113002214p:plain

対象範囲の観点

対象範囲の観点というのは、「閲覧」のときであれば「どこまで見ていいか」という意味での範囲です。例えば大きな病院で医者が使うシステムがあって、「患者一覧」を閲覧するAPIがあったとします。このとき、患者はきっとたくさんいますが、全ての患者の情報を全ての医者が問答無用に閲覧できてしまって良いでしょうか?答えはきっとNOです。単純な例だと、きっと医者が担当している患者の情報しか閲覧できない、みたいな制限があるはずです。これは下図のようなイメージになります。

f:id:kenev:20200107225607p:plain

この観点は主にデータを取得する際に適用するものです。

対象項目の観点

対象項目の観点とは、例えば「個人情報のクレジットカード番号は非表示にする」と言うような、「ある対象の具体的な項目」に関する観点です。この観点をどこで適用するのか、というのは未だに僕も悩んでいるのですが、APIであればデータを返却する直前(Presenter層)なのかなと今のところ思っています。

f:id:kenev:20200113003356p:plain:h300
クレジットカード番号を非表示にしている例

参考

3つの観点に関して、似たようなことが以下のStack Overflowでも述べられているので合わせて読んでみると知識が広がります。

stackoverflow.com

権限のソフトウェアアーキテクチャ

何も考えずにアプリケーションの中に権限を実装すると、ビジネスルールとして実装しているのか、権限の関心事として実装しているのか訳がわからなくなってしまいます。(考えながらやっていても訳が分からなくなっているのが僕の現状

権限のアーキテクチャを考える上で参考になったのがXACML(eXtensible Access Control Markup Language)です。XACMLに関しては僕もほとんどわかっていないのですが、「権限制御用のXMLで標準化されている言語仕様」、という認識です。XACMLの詳細については説明することができないのですが、XACMLのアーキテクチャが権限における役割を理解するために非常にわかりやすかったです。以下、Axiomaticsの記事がわかりやすく説明しているのと、動画もあるので英語に抵抗が無い人はぜひ見てみてください。

www.axiomatics.com

記事内に登場する以下の図が特に重要です。

f:id:kenev:20200107225133p:plain:h300
"XACML Reference Architecture" より抜粋

図にあるように、権限のアーキテクチャにおいて重要な役割が4つあります。

  • 権限を管理(Manage)する役割
    • PAP(Policy Administration Point)
  • 権限の判断を下すサポート(Support)をする役割
    • PIP(Policy Information Point)
    • PRP(Policy Retrieval Point)
  • 権限を見て判断を下す(Decide)役割
    • PDP(Policy Decision Point)
  • 権限を適用(Enforce)する役割
    • PEP(Policy Enforcement Point)

この4つの役割を意識することで、権限のアーキテクチャは結構わかりやすく疎結合に組み立てることができます。

僕が理解している範囲でそれぞれの役割をもうちょっと詳しく見てみます。

権限を管理(Manage)する人

f:id:kenev:20200113103312p:plain

ここは読んで字の如く、権限を管理する箇所です。上の図にあるようにPAPからPRPに、権限にまつわる情報が保存されます。AWSで言うところのIAM Management ConsoleもPAPと言えます。

権限の判断を下すサポート(Support)をする人

f:id:kenev:20200113103329p:plain

ここは権限の判断に必要な付加情報を取得する箇所だと僕は思っています。PRPは権限の情報を取得・保持する箇所なので、権限判断で使う情報としてはサポートというよりは核となる部分だと思うのですが、PIPで取得する付加情報が権限の判断を下すサポートをするメインの箇所という認識です。現に、PRPを表現せずに、PIPだけが表現されている図もよく見かけます。ここで言う付加情報とは、例えばAPIのリクエストにはJWTだけが送られてくるけど、そのJWTに紐づくユーザーの属性値(年齢・所属グループ)などのことです。これをPIPが外部リソース(API・DB・LDAPなど)から取得し、権限の判断のためのサポートを行うというわけです。

権限を見て判断を下す(Decide)人

f:id:kenev:20200113103519p:plain

権限の中ではここが一番中心となる部分でしょう。PAPで管理している権限がPRPに保持されていて、PIPからの付加情報と合わせて最終的な判断を下すのがここ(PDP)です。ここが「やっていいですか?」のような質問を受け付ける場所で、「いいよ」あるいは「だめ」という回答をする場所です。

判断の回答は単純ではない

上に「いいよ」と「だめ」といった単純な回答を例にしましたが、実際のところこのような単純な回答だけでは要件を満たせないことが多いと思います。先の例にも使った「医者は担当している患者を閲覧できる」という権限があったとします。PDPがもし「いいよ」と「だめ」しか回答できなかった場合、この要件を満たすためには患者レコードを全部取得して、一つずつに対して「患者Xのデータを閲覧していい?」という質問をPDPにしなければいけません。10,000レコードあれば、10,000回質問する必要があるということです。

これではスケールしません

ではどうすればいいのか?それは、クローズドな質問ではなく、オープンな質問ができるようにする必要があります。クローズドな質問が「患者Xのデータを閲覧していい?」というのに対して、オープンな質問は「どの患者を閲覧していい?」というものです。そして、この回答が「担当している患者であればいいよ」というものになります。つまり、回答は「いいよ」や「だめ」と言った単純なものではなく、もっと複雑なものになります。具体的には、例えばデータストアがRDBだった場合に、回答がSQLそのものである可能性もありえます。

実際にこのソリューションを提供しているAxiomaticsのARQ(Axiomatics Reverse Query)が以下の動画で紹介されていて、非常に興味深いです。

www.youtube.com

また、下でも紹介しますが、Open Policy Agentの以下の記事に関しても似たようなことを実現しようとしていて興味深いです。

blog.openpolicyagent.org

この観点に関して、言っていることはわかったとしても「じゃあ実際どう実装するの?」という点に関しては結局僕も答えが見つかっておらず、模索中です。少なくともAxiomaticsのようなソリューションを自分たちで作るのは非現実的だと思います(Axiomaticsはそれを専門にビジネスにしている会社なので)。また、そもそも「回答をSQLにする」というのも、権限の内容とデータベースの項目が密結合すぎて繊細になりすぎるのでは、とも思っています。

権限を適用(Enforce)する人

f:id:kenev:20200113103551p:plain

最後に重要なのが、権限の質問をして回答をもらった後に権限を適用する箇所で、それが上図でいうPEPとなります。APIであれば、「いいよ」という回答がもらえたら処理を続行し、「だめ」と言われたら403 Forbiddenを返す、といった処理が「権限を適用」していることになります。

権限実装のアプローチ

実際に権限はどうやって実装すると良いのか、というのもまた悩みの種です。「対象範囲の観点」で権限を適用するときを例にとって、実装のアプローチについて挙げてみます。

ハードコードするアプローチ

権限実装においてハードコードと言われて何のことなのかイメージできるでしょうか?僕は最初は全くイメージできていなかったです。僕の中での当初のハードコードというのは下のようなコードです。

// '100'って書いちゃってることがハードコードだと思っていた
if (employee_id === '100') {
  execute();
}

このイメージが一般的には多いんじゃないかと思います。しかし、権限実装におけるハードコードというのは少し毛色が異なります。現状の僕の理解は以下のとおりとなっています。

権限実装における「ハードコードしている状態」とは、権限の内容が変わった場合に、権限の関心以外のコードが影響を受けてしまう状態

ふわっとしててわかりづらいと思うのですが、具体例をあげます。先に述べた患者一覧を取得するAPIについて考えてみます。「患者一覧を取得する」APIであれば、単純に考えたらエンドポイントは /api/patients のようなものになるのが想像できると思います。そしてこのリソースをDBから取得する場合のコードはきっと下のようになります(JSっぽい擬似コードです)。

function findAllPatients() {
  return query.execute('SELECT * FROM patients');
}

ただし、これでは本当に「全患者を取得」してしまいます。「見て良い範囲の権限」が適用されていません。先の例では「担当している患者だけ閲覧して良い」という要件があるので、この権限をコードに適用すると下のようになります。

// doctorIdにはリクエストした医者のIDが入る
function findAllPatients(doctorId) {
  return query.execute(
    'SELECT * FROM patients p WHERE p.doctor_id = ?',
    doctorId);
}

この WHERE p.doctor_id = ? と書いた場所が権限実装における「ハードコードしている箇所」になります。一見 ? には動的に医者のIDが入るので、ハードコードしているようには見えないのですが、権限の内容が変わった場合のことを考えてみましょう。例えば、「担当している患者だけ閲覧して良い」という権限から「同じ診療科内の医者が担当している患者だけ閲覧して良い」に変わったとします。こうなると、下のようなコードに変える必要がありそうです。

// doctorIdにはリクエストした医者のIDが入る
function findAllPatients(doctorId) {
  // 医者の所属診療科を知りたいので医者の情報を取得する
  doctor = query.execute(
    'SELECT * FROM doctors d WHERE d.id=?',
    doctorId);
  // 医者の所属する診療科内の医者ID一覧を取得する
  doctorIds = query.execute(
    'SELECT id FROM doctors d WHERE d.department_id=?',
    doctor.departmentId);
  // 患者の担当医が上で取得した医者IDと一致するものだけ取得する
  return query.execute(
    `SELECT * FROM patients p
      WHERE p.doctor_id IN (${doctorIds.join(',')})`);
}

これが権限を「ハードコード」してしまっているがために、本来影響を受けてほしくないfindAllPatientsの実装に変更が発生してしまっている状態です。患者一覧を取得する関数(findAllPatients)のはずが、権限の関心が紛れているせいで権限の内容が変わった場合に影響を受けてしまう、というものです。

「いや、要件が変わってるんだから影響受けてもいいでしょ」

と、思うかもしれませんが、これが医者によって閲覧できる範囲が変わったりしたらどうなるでしょう?例えば「一般権限の医者は担当している患者を閲覧できる」が、「マネージャー権限をもつ医者は同じ診療科内の医者が担当している患者を閲覧できる」といった違いがある場合です。別々の関数を用意して、マネージャーなのか、そうじゃないのかで使い分けたりしそうな複雑な実装になりそうです。

function findAllPatients(doctor) {
    patients = [];
    if (doctor.role === 'MANAGER') {
        // マネージャーだったら
        patients = findMyDepartmentsPatients(doctor.id);
    } else {
        // マネージャーじゃなかったら
        patients = findMyPatients(doctor.id);
    }

    return patients;
}

// 担当している患者を取得
function findMyPatients(doctorId) {
  // ...
}

// 同じ診療科内の医者が担当している患者を取得する
function findMyDepartmentsPatients(doctorId) {
  // ...
}

ユースケースとしては「患者一覧タブを表示したら患者一覧を取得する」というシンプルそうに聞こえるものなはずなのですが、findAllPatients関数の中身を見たら「なんだか思ったより複雑なことをしている」という状態になっているのが想像できると思います。

f:id:kenev:20200110235602p:plain:h200
患者一覧を表示する画面のイメージ(ユースケースはシンプルに見える)

コアロジックから切り離すアプローチ

ということでハードコードのアプローチは小規模〜中規模なアプリケーションまでなら問題無い気がしますが、中規模〜大規模なアプリケーションになってくるとコードの可読性がしんどくなってくるかと思います。そこで、権限の関心の分離をする方法がもっとスケールするのかなと思います。

このアプローチには上の例で示しているような「生のSQLを書く」やり方は厳しいんじゃないかと現状の僕は思っています。SQL Query Builderのような、生のSQLをWrapするようなものがあって、SQL実行前に介入しつつ、権限を適用する仕組みが必要だと思っています。

僕は身近でLaravelを使っているので、LaravelのQuery Builderの力を借りると下のコードのように権限とコアロジックの分離ができそうです。患者を取得するORM(Eloquent Model)をPatientとすると:

<?php

// ...

// 患者一覧を取得
$patients = App\Patient::enforcePolicy()->get();

enforcePolicyというのがLaravelのScopeを使っていて、モデル内では下のように実装されています(「担当している患者」に絞り込んでいます)。権限の内容に変更があった場合は、下のscopeEnforcePolicyの中身を変えるだけで済み、コアロジック側には影響が無いという状態にすることができます。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Patient extends Model
{
    /**
     * @param  \Illuminate\Database\Eloquent\Builder  $query
     * @param  String  $doctorId
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeEnforcePolicy($query, $doctorId)
    {
        // 担当している患者だけに絞り込む
        return $query->where('doctor_id', $doctorId);
    }
}

ただし、現時点ですでに課題と感じている点としては

  • App\Patient::enforcePolicy()->get()enforcePolicy()を呼び出している時点で明示的にORMを使う側が権限を意識しないといけないので、結構繊細なコードになってしまう(enforcePolicy呼び出し忘れたら全取得できてしまうセキュリティリスクがある)
  • ↑に似てますが、PatientモデルにscopeEnforcePolicyという実装を入れているので、Patientが権限を意識する必要がある。さらに隠蔽化、あるいは処理を移譲してPatientが意識しないようにするべき

ということです。フレームワークレベルでさらにいい感じに実装できるものがありそうですが、現状では僕はそこまで把握できておらず、イメージ的にこのような分離ができたら素敵なのでは、と妄想しているだけです

権限はビジネスルールの関心事なのか

上で色々と言っておきながらそもそも論な疑問になってしまうのですが、Clean Architectureだったり、ドメイン駆動設計にも挑戦している中、「権限はビジネスルールの関心事なのか」という疑問へのスッキリする答えが未だに見つかっていません。少なくとも上に述べている実装のアプローチは、ビジネスルールと権限の関心を分離しているアプローチのつもりです。なので、権限の変更によってビジネスルール側(ドメイン)には影響が無いようにしています。

Clean Architectureのボブおじさんも、Clean Code Discussionにおいて以下のように発言しています。

Security is an application specific concern, it belongs to the interactors. The controllers would access the current user's credentials and pass that information to the interactors. The interactors would use an authorization service to ensure that their particular interaction was authorized. The business objects wouldn't know anything about it.

「セキュリティはアプリケーション特有の関心事であり、ビジネスオブジェクトはこのことについて意識しない」と言っています。なのでこの言葉をそのまま鵜呑みにすると、「権限の関心をビジネスルール内に持ってこない」考え方は間違っていないのかなと思っています(セキュリティに関連するアプリケーションを作っているのであれば話はきっと違う)。

この観点に関しては色々な人の色々なアプリケーションでの色々な実装方法を聞きたいです。

システムアーキテクチャ

どこまで権限の関心を分離するのか、と考えたときに意識しないといけないのがシステムアーキテクチャです。モノリシックとマイクロサービスなアプローチについて言及し、それぞれのメリット・デメリットについて述べたいと思います。

モノリシックなアーキテクチャ

迷った場合にまずとってみるアプローチがモノリシックなアーキテクチャかと思います。下図のように、上に出てきたPEP, PDP, PIP, PAPをすべて同じアプリケーションに詰め込むことになります。

f:id:kenev:20200111235737p:plain:h300

それぞれの○が別々のアプリケーションサービスになっていくのがわかりやすい分け方かなと思っています。

考えられるメリット

  • 一つにまとまっているので管理がしやすい(はず)
    • 一つのコードベースなのでコードが追いやすい
    • CI/CDなど開発プロセスもシンプル

考えられるデメリット

  • プログラミング言語の縛りが発生し、言語に依存したライブラリしか使えないことになる
  • 権限に変更が発生すれば基本的にアプリケーションをデプロイし直す必要がある
  • 他のアプリケーションが同じ権限基盤を使いたい場合に、不必要なアプリケーション間の依存ができてしまう

参考

権限の実装をするにあたってスクラッチから全部作るのは大変なので、Casbinのようなライブラリの力を借りながら実装していけるのかなと思っています。

casbin.org

マイクロサービスなアーキテクチャ

f:id:kenev:20200112230522p:plain:h300

運用上の懸念点が増える一方で、最も権限の関心の分離が行えるのがこのマイクロサービス的アプローチになるかと思います。PEP以外(PDP, PIP, PAP)の権限に関連する登場人物はすべて別アプリケーションにします。

考えられるメリット

  • 権限アプリケーションに最適な言語・ライブラリを選択できる
  • 権限アプリケーションとその他アプリケーションのデプロイストラテジを分離できる
  • 権限アプリケーション単体でスケールできる

考えられるデメリット

  • 全体的に管理が複雑になる
    • コードベースの分離
    • ソフトウェアアーキテクチャが複雑になりがち
    • 別アプリケーションであるがための懸念事項の増加(特にネットワーク周り)

参考

Open Policy Agent

CNCFのプロジェクトでもあるOpen Policy Agentがかなり興味深い存在です。Open Policy Agentにはサーバーモードがあるので、Out of the boxでそのまま別アプリケーションとして使うことができます。そのままの機能では不十分だと感じたら、Goのライブラリとしても使うことができるので、強力な権限アプリケーションをGoで自作することも可能です。

www.openpolicyagent.org

また、権限用の独自の言語Regoを扱っていて、権限を宣言的に定義することができます。Open Policy AgentやRegoに関してはこのブログでも何度か紹介しているので、気になる方はぜひ読んでみてください。

kenfdev.hateblo.jp

Authzforce

僕は使ったことがないのですがXACMLに準拠していてOSSなもので名が通っているものとしてAuthzforceというものがあるようです。Community Editionもあるみたいなので、評価してみる価値があると思います。

authzforce-ce-fiware.readthedocs.io

Axiomatics

有償にはなりますが、Axiomaticsのソリューションを使うという選択肢もありそうです。XACMLのラッパー言語としてALFAを使うことができ、宣言的に権限を定義することができます。Kubernetes上にもデプロイできるようで、PayPalも使っているようです。

www.axiomatics.com

余談ですが、権限周りのStackOverflowの質問に対して、ものすごくわかりやすくて良い回答をしている人がいると思った場合にユーザーを見てみると、だいたいAxiomaticsのDavid Brossard氏です。権限の知識を増やしたければフォローしておくとすごく勉強になります。

stackoverflow.com

モダンなフロントエンドにおける懸念事項

忘れがちなのがフロントエンドの観点です。SPA(Single Page Application)登場前であればフロントの生成(HTML)もサーバーサイドが担当していたので、サーバーサイドが持っている権限の情報をフロント側にも使えば良いというものでした。

LaravelのPolicyをBladeテンプレートで使う例がこのやりかたに当たります。他のフレームワークでも似たようなことができるはずです。

@can('update', $post)
    <!-- The Current User Can Update The Post -->
@elsecan('create', App\Post::class)
    <!-- The Current User Can Create New Post -->
@endcan

@cannot('update', $post)
    <!-- The Current User Can't Update The Post -->
@elsecannot('create', App\Post::class)
    <!-- The Current User Can't Create New Post -->
@endcannot

laravel.com

しかし、最近はReact, Vue.js, AngularなどのSPAを使うことが増えてきたため、フロントとサーバーサイドが分離しています。ということは、権限情報は標準でフロント側は持っていないということを意識しておく必要があります。やり方としては大きくは2つあるのかなと思っています。下図のような「編集機能付き患者一覧画面」を例にとって、どういうアプローチがあるか見てみます。

f:id:kenev:20200112113704p:plain
編集機能付き患者一覧画面

リソース自体に権限の内容を埋め込む方法

この方法が一般的には多い印象があります。APIのレスポンスが以下のような形になります。

[
    {
        "id": "1",
        "name": "患者A",
        "doctorName": "医者A",
        "editable": true
    },
    {
        "id": "2",
        "name": "患者B",
        "doctorName": "医者B",
        "editable": false
    },
    {
        "id": "3",
        "name": "患者C",
        "doctorName": "医者A",
        "editable": true
    }
]

ここに登場するeditableのキーを「権限情報がリソースに埋め込まれてる状態」と僕は呼んでいます。「患者」のリソースを取得できるAPIのはずが、患者とは関係ない「編集可能かどうか(editable)」という権限の情報もまぎれこんでいるのがわかります。

考えられるメリット

  • フロントは圧倒的に実装しやすい(上の例であればeditableを見るだけで編集して良いかどうかが判断できる)

考えられるデメリット

  • リソースに権限の情報が紛れ込んでしまう
  • ↑の影響でサーバーサイドの「患者一覧取得」APIは、患者の情報以外に権限の情報をJSONに付加する必要がある

権限情報をリソースと切り離す方法

この方法は馴染みがあまり無いという人が多いのではないかと思います。このアプローチの場合、APIから取得するリソースの情報に原則として「権限情報」を含めません。権限情報はAPIを用意することになります。

まず、患者一覧を取得するAPIJSONは下のようになります。

[
    {
        "id": "1",
        "name": "患者A",
        "doctorName": "医者A"
    },
    {
        "id": "2",
        "name": "患者B",
        "doctorName": "医者B"
    },
    {
        "id": "3",
        "name": "患者C",
        "doctorName": "医者A"
    }
]

前の例との違いは、権限情報であるeditableが消えたことです。では編集可能な患者はどのように見分けるのでしょう?そのために別途権限情報を取得するAPIを用意します。権限のAPIに対しては「私は何ができますか?」という問い合わせをするイメージです。以下は権限APIのレスポンスJSONの一例です。

[
  {
      "action": "read",
      "resource": "patient"
  },
  {
      "action": "edit",
      "resource": "patient",
      "condition": {
          "doctorName": "医者A"
      }
  }
]

読み替えると、「すべてのpatientリソースの閲覧ができる」ことと「doctorName医者Aとなっているpatientリソースなら編集できる」、という内容になります。

下図のように、2つのAPIから取得した情報を組み合わせることでUIを作ることになります。

f:id:kenev:20200112220119p:plain

考えられるメリット

  • API設計時に、リソースに対する権限の関心を基本的には考えなくて良くなる
    • 例:患者取得APIで、レスポンスのJSONeditableの値に何を入れるか気にしなくて良い
  • 関心の分離のおかげでコードの保守性が高まる(はず)

考えられるデメリット

  • 良くも悪くも、権限の関心が分離していることによる設計のコストだったり学習コストはかなり高くなる
  • フロントの実装コストが高くなる(権限APIと、対象となるリソースを組み合わせてUIの挙動を考える必要がある)

参考

権限の分離を行っているアーキテクチャを支援してくれるライブラリだったり、ドキュメントがいくつかあるので紹介します。

CASL

CASLというライブラリが主要なJSフレームワークと連携して使うことができます。

stalniy.github.io

何に対してどんなアクションを、どんな時に実施して良いかという権限の関心を独立して管理することができます。参考まで、以下が実際のJSONでの定義例です。

[
  {
    "actions": ["create", "read", "update", "delete"],
    "subject": "Post",
    "conditions": {
      "author": "1"
    }
  },
  {
    "actions": ["read", "update"],
    "subject": "User",
    "conditions": {
      "id": "1"
    }
  }
]

1つ目の定義は「Postのauthorの値が"1"であれば、create, read, update, deleteのアクションを許可する」というもので、2つ目の定義は「Userのid"1"であれば、read, updateのアクションを許可する」というものです。この情報をUI側から使って、例えば「編集ボタンを表示・非表示」にすることができます。

Chef Automate

このトピックについて説明しているドキュメントとしてChef AutomateのAuthorizationコンポーネントがあります。特にIntrospectionの節がこれにあたります。権限設計含めて神がかっているドキュメントだと思うので、ぜひ一度読んでみることをおすすめします。

github.com

権限設計はどこを目指したら良いのか

ロバストな権限設計を!」と言われても「権限実装 ベストプラクティス」とググったところで「権限」のカバーしている領域が広すぎて求めている情報がなかなか見つかりません。その中で僕が今の所目指している方向性としては「AWSのIAMのような仕組み」です。

理由としては:

  • 上にも何度が言及しているOpen Policy AgentのSlackで度々「AWSのIAMのような仕組みを作りたい」って人が登場する
  • Open Policy Agentを使っているChef Automateも方向性が似ている(ただしConditionという概念はまだ無い)
  • Open Policy Agentを使っているory/ketoの仕組みもAWSのIAMに似ている(Conditionの概念もある)
  • IAM Policyの共通のJSONフォーマットがわかりやすいし、先人の知恵がつまっている
  • AWSのIAMに関するドキュメントが権限設計においてすごく勉強になる

というように、権限設計・実装をするためのとっかかりがあった、という感じです。ただ、僕自身がOpen Policy Agentにはもともと期待しているというバイアスが働いてるのも理由としては大きいと思います。

完全に「AWSのIAM」を作るところを目指すのは非現実的ですが、現実的なラインを見極めながらシンプルで良いところを自分のシステムに組み込んでいくアプローチが良いんじゃないかと思っています。

本当に強力な権限サービスが必要であれば、Axiomaticsのような有償サービスに移譲することも有用な選択肢の一つだと思います。これに関してはコストとユースケースのフィットを自分なりに見極めるしかないでしょう。

最後に

ここまで読んでいただきありがとうございます。そして、まとまりの悪い内容になっていることを再度お詫びいたします。ただ、同じく権限設計に悩む誰かのヒントになったり、同じように悩んでいる人がいるんだよという共感を持てる内容にどこかしらなっていたら幸いです。

今後もこの記事をアップデートしていくつもりですが、独りで苦悩していてもつらいので、権限設計Meetupとかあったら面白いんじゃないかな、と思いました。色々な人の泥臭い話が聞ける気しかしないです。また、「Architecting Authorization」みたいな本が出てほしいなという気持ちもあります。本当に。。。世の中のエンジニアはどうやってこの苦悩と闘っているのでしょう?

今の僕はというと、Open Policy AgentのSlackを見ながら、同じように苦悩している人の会話を日々追っています。興味がある人はぜひJoinしてみてください。

slack.openpolicyagent.org

長々と書いてしまいましたが、いったんここまでとして引き続き精進し続けたいと思います。

その他参考情報

AWS IAMの「ポリシーの評価論理」に関するドキュメント

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_policies_evaluation-logic.html

AWS IAMに関する丁寧な解説と、最後にはABACの例も紹介されている動画

www.youtube.com

AxiomaticsがRBACからABACへのパラダイムシフトについて詳しく述べている記事

www.axiomatics.com

IAMに関する概要説明をAxiomaticsのDavidが回答しているStack Exchange

security.stackexchange.com

ABACの実装についてAxiomaticsのDavidが説明している記事

www.axiomatics.com

AxiomaticsがDynamic FilteringとDynamic Maskingについて述べられている記事

www.axiomatics.com

AxiomaticsがDynamic AuthorizationをDemoとともに丁寧に解説している動画

www.youtube.com