OPAに関する記事まとめ

以下はこのブログ内のOpen Policy Agentに関する記事を一覧化したものです。

入門

kenfdev.hateblo.jp

kenfdev.hateblo.jp

Regoの実装例

kenfdev.hateblo.jp

kenfdev.hateblo.jp

kenfdev.hateblo.jp

kenfdev.hateblo.jp

Regoのユニットテスト

kenfdev.hateblo.jp

機能の紹介

kenfdev.hateblo.jp

CIと連携

kenfdev.hateblo.jp

kenfdev.hateblo.jp

kenfdev.hateblo.jp

kenfdev.hateblo.jp

kenfdev.hateblo.jp

カンファレンス関連

kenfdev.hateblo.jp

その他

kenfdev.hateblo.jp

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

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

ConftestとCircleCIで継続的に構造データにPolicyを適用する

構造化されたデータにPolicyを適用したい、って気持ちはチームでコラボレーションしていたら自然と生まれてくる欲求だと思います。コードの場合は最近だと優秀なLinterがあるおかげである程度は強制することができるのですが、例えばYAMLだったりiniファイルだったりはどうでしょう?これらの構造化されたデータに対して、自分で作ったPolicyを適用できたら素敵だと思いませんか?そこで取り上げたいのがConftestです。以前このブログでも紹介したことがあるので、初めて聞いたという方はこちらの記事も合わせて読んでみてください。

kenfdev.hateblo.jp

今回はConftestをただ単体で使うのではなく、CircleCIと組み合わせて継続的にPolicyを適用する方法について見ていきます。これを実現したいがためにconftest-orbというCircleCIのOrbを作りました。

github.com

概要

CircleCIでconftest-orbを使った最小限のYAMLは以下のようになります。

version: 2.1
orbs:
  conftest: kenfdev/conftest-orb@x.y # バージョンは指定してください
workflows:
  build:
    jobs:
      - conftest/test:
          pre-steps:
            - checkout
          file: config_to_test.yaml

前提条件は以下の通りです:

これらを守った上でconfig_to_test.yamlに対してPolicyを適用します。

Serverless FrameworkのYAMLに適用してみる

では、もうちょっと具体例をあげてみます。Serverless FrameworkのYAMLを例にとって、Policyを実際に適用してみます。以下のリポジトリに実際にソースを置いています。

github.com

.circleci/config.ymlに設定しているCIの内容は以下のとおり:

version: 2.1
orbs:
  conftest: kenfdev/conftest-orb@0.0.8
workflows:
  build:
    jobs:
      - conftest/test:
          pre-steps:
            - checkout
          file: serverless.yaml

リポジトリの構成も、上で述べている前提条件を守っています。

kenfdev/conftest-serverless-circleci
├── policy
│   ├── base.rego
│   └── util.rego
└── serverless.yaml

そして、Policyを適用する対象となるserverless.yamlは以下のような内容になっています。

service: aws-python-scheduled-cron

frameworkVersion: '>=1.2.0 <2.0.0'

provider:
  name: aws
  runtime: python2.7
  tags:
    author: 'this field is required'

functions:
  cron:
    handler: handler.run
    runtime: python2.7
    events:
      - schedule: cron(0/2 * ? * MON-FRI *)

今回適用するPolicyの詳細(Regoファイル)についてはこの記事では見ません。興味がある方は実際に見てみてください。概要としては次のとおりです。

  • provider.tagsauthorを設定していること
  • provider.runtimeにPython2.7を設定していないこと
  • functionsruntimeにPython2.7を設定していないこと

上のYAMLと照らし合わせて、最初のPolicyは守られているけど、最後の2つに関しては守られていないことがわかります。この状態でCircleCIを走らせると以下のようにちゃんと叱られます。

f:id:kenev:20191218175806p:plain

Policyを一箇所で管理する

ここまでで、いい感じにCIでPolicyを適用できることがわかりました。ただ、結構大きな問題点があります。それは、適用したい構造データと同じリポジトリにPolicyも管理している点です。Policyは再利用するケースが多いと思うので、上で用意したようなPolicyをすべてのリポジトリで管理なんてしていられません。ということでここでConftestの強力な機能であるPolicyのpushとpull機能を紹介します。

v0.15.0時点でpush先、あるいはpull先として使えるのはOCIレジストリになります。Docker Registryが身近で使えるOCI互換のレジストリなので、これをどうにか使えないか試行錯誤してみました。

かなり斜め上な使い方をすることになってしまいますが、GitHub上にPolicyを保存して、更新するたびにこのPolicyを含んだDocker Registryのコンテナイメージを作るようにしてみました。流れのイメージとしては下図のとおりとなります。

f:id:kenev:20191218180955p:plain

この仕組みを使ってCircleCIのOrbに関するベストプラクティスが適用されているか、継続的にチェックしたいと思います。Policyは以下のリポジトリで管理しています。

github.com

Policyを含んだDocker Registryがあることで、CircleCI上でConftestを実行する際に一箇所で管理されているPolicyを再利用して適用することができます。

conftest-orbの開発フローで面白い点は、自分自身のCIにCircleCI Orbのベストプラクティスを継続的に適用していることです。ドッグフーディング状態だと言えます。

そんなconftest-orbのCIは以下のように定義しています。

jobs:
  general_usecase_test:
    executor: machine
    steps:
      - checkout
      - circleci-cli/install
      - run:
          name: Pack the orb.yml
          command: circleci config pack src > orb.yml
      - conftest/install
      # start the OCI registry(this command is declared in a different place)
      - start_oci_registry:
          image: kenfdev/circleci-orbs-policies
      # pull the policies from the OCI registry
      - conftest/pull:
          policy_path: policy
          repository: 127.0.0.1:5000/policies:latest
      # test with minimum options
      - conftest/test:
          policy_path: policy
          file: orb.yml

ちょっとグチャっとしている点もありますが、それはCI中にDocker Registryを起動する必要があるからです。外部にすでにOCI Registryがあれば、下のようにすっきり書くことができます。

version: 2.1
orbs:
  conftest: kenfdev/conftest-orb@0.0.8
workflows:
  build:
    jobs:
      - conftest/test:
          pre-steps:
            - checkout
          repository: <path-to-your-oci-registry>
          file: serverless.yaml

<path-to-your-oci-registry>からPolicyをpullしてConftestを実行することができます。

このように、Policyを一箇所で管理しつつ、複数のリポジトリに対してCI経由で継続的に適用するということが比較的観点に実現することができます。Conftestのexampleを見ると、下の一覧のようにかなり様々なユースケースに適用できるということがわかるので、興味がある方はぜひconftest-orbも使ってみてください!

  • awssam
  • compose
  • configfile
  • docker
  • hcl2
  • ini
  • kubernetes
  • serverless
  • tekton
  • terraform
  • ts
  • xml
  • etc.

まとめ

  • conftest-orbを使ってCircleCIで継続的に構造データにPolicyを適用できる
  • OCI Registryを使ってPolicyを一箇所で管理して共有することができる

以下のPRもマージされたため、v0.16.0以降ではOCI Registryではなく、HTTPでPolicyを外部から取得できるようになるので、さらに簡単にPolicyを適用できるようになりそうです!

github.com

関連

kenfdev.hateblo.jp

「ディオ・ブランドーに学ぶ失敗論」に参加しました

ply-osaka.connpass.com

ディオ・ブランドーに学ぶ失敗論」というかなりエモい勉強会に参加してきました。

きっかけ

きっかけはうちのCTOのこのツイートから。

「どうせ東京なんでしょ」って思ってイベント見てみたら、なんと大阪!これは行くしかないと思って参加することにしました。

概要

目次

内容は下のように構成されていました。前半はジョジョを知らない人でもわかるように、わかりやすく端的に解説していただけます。

  • ジョジョの奇妙な冒険』とは?
  • ジョースター家 最大の宿敵 ディオ・ブランドー
  • ディオ・ブランドーに学ぶ失敗論
    • 失敗論=失敗を論じること
    • 失敗とは?
    • 「目的」の展開
    • ディオの失敗
      • 目的(やりたいこと)
      • 理由(根底にあるもの)
      • 方針(考え方、戦略)
      • 方法(行動、戦術)
      • 結果・成果 ⇒ 失敗
        • ディオはどこで失敗したのか?
        • 承太郎 vs ディオ
          • 似た者同士の戦い
          • 決定的な2つの差
      • 今回の結論:失敗とは?

ディオの失敗

イベントの詳しい内容については文章でうまく伝えられる自信が無い&その場にいるからこそ楽しいと思える非常に新鮮なタイプのイベントだったため、割愛します。

「なぜディオは失敗したのか?」

という点については以下がポイントだったと述べられていました。

  • 間違った方向に行動し続けてしまった(ふりかえりが無かった)
  • 楽をしようと挑戦から逃げていた(挑戦し続けることは大事)

この結論にいたるために非常に詳しく、様々な観点から漫画内のシーンを抽出して裏付けがされています。スライドを共有することができないのが非常に残念です(笑

最後に、

真の「失敗」とはッ!
開拓の心を忘れ!困難に挑戦する事に無縁のところにいる者たちの事をいうのだッ!

- Part7 第1巻「#1 スティール・ボール・ラン記者会見」

という引用から、

失敗⇔成功

ではなく

失敗⇔挑戦

であると主張し、発表は幕を閉じました。

イベントについて

発表内容

非常にうまくまとまっていて面白い内容でした!スライドがまたいい感じにまとまっていて、めちゃくちゃ欲しいと思いました。公開されることはまず無いと思うので、イベント参加者しか見ることができない非常に貴重なものです。関西在住の方、ぜひ参加してみましょう!

参考までに以下は、今回の失敗論に関する発表内のスライドに出てきたキャラクターたちです。(※特に引用なくコマだけ登場してるキャラクターも含めてます)

参加者の自己紹介タイム

発表開始前に参加者の自己紹介タイムを制限時間1分で行ったのがすごく良いと思いました。自己紹介する内容もある程度縛りがあり:

  • 自己紹介
  • ジョジョ
  • 好きなキャラクター・スタンド(その理由)
  • イベント参加のきっかけ

というものでした。参加者がどんな人なのか、自分の近くにいる人はどういう人なのか、ちょっと知っている状態でイベントに参加するとかなりリラックスできることがわかりました。これは真似したいです。

参加のためのハードル

マニアックな内容になりそうなタイトルとは裏腹に、参加者のジョジョ歴は様々でした。最近のアニメから漫画に入った人もいれば、昔から読んでいて全巻所持している人もいて、ジョジョをほとんど知らない方も参加されていて非常に面白いバランスのとれた参加者たちでした。

おわりに

僕はテックな勉強会ばかり参加してきていたので、今回のイベントはかなり違う雰囲気を味わうことができて新鮮でした。初めて会う人たちが多い中でもイベントが盛り上がったのはジョジョのすごさと、イベント進行の素晴らしさがかけ合わさったものなのかなと思いました。

次回は「ジョナサン・ジョースターに学ぶ人生の成功法則(ジョジョ×『7つの習慣』)」で11月8日(金) 開催です!興味がある方は参加してみましょー!

docs.google.com

Percy + CypressでVisual Regression Test

自動テストの中にやっとの思いでe2eテストを組み込んだとしても、「視覚的に正しかったかどうか」という点までキャッチするのはなかなか難しいです。このような観点でテストする一つの手段として「Visual Regression Test」があります。僕自身このテストにはVue Fes Japan 2018の以下の発表で知りました。

ここで紹介されていた「reg-suit」が気になって、自分のローカル環境でも検証してみたこともあります。

github.com

medium.com

今回共有したい内容はこのVisual Regression TestのSaaSであるPercyです。

percy.io

たまたまTwitterの広告で流れてきたのを見たのですが、最近僕が使っているe2eテストフレームワークCypressとも連携できるとのことで気になって試してみました。

参考にしたのは以下のYoutubeの内容で、Percy.ioのCEOである@mikefotinakisとCypress.ioのVPoEである@bahmutovによるCypress + Percyの紹介になります。

www.youtube.com

動画内の以下スライドも非常に参考になります。

slides.com

想定する読者

  • e2eテストなんとなくわかる(Cypressなんとなく知ってる)
  • CIなんとなくわかる(CircleCIなんとなく知ってる)

細かい説明はごっそり省くので、この記事は初心者向けの記事ではありません。また、上記Youtubeが理解できる人はこの記事を読まなくて良いと思います。

概要

細かく一つ一つ説明しているとこの記事がとんでもなく大きくなってしまうので、ざっくりと登場人物と役割を説明します。

テスト対象としては動画内のものと同様、以下のサンプルアプリ(ピザ注文アプリ)を使います。

github.com

f:id:kenev:20190825090330p:plain
angular-pizza-creator

e2eテストのテスト項目としては

  • ユーザーはピザを注文できる

というものがあります。ここで活躍するのがCypressです。画面のボタンをポチポチしながらピザが注文できることをテストします。

このテストで確認することが困難なのが、

  • アプリが正しく表示されているか

という観点です。ここで活躍するのがPercyです。(どのように活躍するのかはこの記事の続きでウォークスルーします)

そしてこれらをGitHubのPRのたびに継続的にテストさせるために活躍するのがCircleCIとなります。

Percyを使うにあたってCypress, GitHub, CircleCIは差し替え可能です

それではPercyを試してみたいと思います!

前提

Percyの準備

Percyの登録

https://percy.ioにアクセスしますして、GitHubアカウントで登録しました。

f:id:kenev:20190825092327p:plain
登録

続いて、Company nameとメールアドレスの登録になるのですが、このCompany nameGitHubでいうOrganizationだったりOwnerみたいな扱いになってそうなので注意が必要です。(Individualにしたら重複エラーになってしまった)

f:id:kenev:20190825092413p:plain

最初のプロジェクトの登録をします。1リポジトリに複数のプロジェクトを登録できるみたいですが、今回はリポジトリの単位=プロジェクトの単位と考えて良さそうです。

f:id:kenev:20190825092700p:plain
初期プロジェクトの登録

これで初回登録のフローが完了しました!

f:id:kenev:20190825092904p:plain

Percyと外部サービス連携

上のキャプチャに表示されている「Add integration」から、外部サービスを連携させることができます。

f:id:kenev:20190825093230p:plain
外部サービス連携

GitHub連携

GitHubとPercyを連携します。GitHub Appをインストールすることになります。

f:id:kenev:20190825093322p:plain
GitHub連携

諸々許可をするとInstalledになります。

f:id:kenev:20190825093417p:plain
GitHub連携完了

連携外部サービスにGitHubが追加されているのがわかります。

f:id:kenev:20190825093524p:plain

Slack連携

Slack連携も簡単に行うことができます。以下の中から何を通知させるか選択できるようですが、とりあえず全部通知させるようにしてみました(通常はUnreviewedだけで十分だと思います)。

  • Unreviewed
    • 画面上で差分が出たもののレビューが必要な場合に通知
  • No changes
    • 画面上で何も差分が無かったものでも通知
  • Auto-approved
    • 自動Approve設定されているブランチでは自動Approveが行われるが、そのタイミングでも通知
  • Approved
    • ビルドがApproveされたときにも通知

f:id:kenev:20190825093734p:plain
Slack連携

以上でGitHubとSlackの連携が終わって以下のようになります。

f:id:kenev:20190825094248p:plain
GitHubとSlack連携完了

CircleCIの設定

PercyとCircleCIを連携します。これに関しては公式ドキュメントでも説明されています。

docs.percy.io

リポジトリ追加

CI連携しておきたいので、CircleCIにリポジトリを追加します。

f:id:kenev:20190825094456p:plain
CircleCIにProject追加

サードパーティ製のorbsを許可

PercyはCircleCIで使えるorbsを提供しています。これを使うためにはCircleCIのSecurity設定からorbsの使用を許可する必要があります。これが設定されていない場合にはCircleCIで以下のようなエラーになります。

f:id:kenev:20190825095030p:plain
ビルド失敗

CircleCIのSettings > Securityの項目で、以下のように設定することでorbsが使えるようになります。ビルドエラーにも書いてありますが、この設定をONにして、CircleCIのJobをRerunしても失敗するので注意!新しいJobを発火させないとビルドエラーは解消されません。

f:id:kenev:20190825095425p:plain
Settins > Security

PERCY_TOKEN環境変数の設定

Visual Regression Testなので、Percyにテスト時の情報を送信する必要があります。そのためにはトークンが必要になるので、PercyのProject Settingsからトークンを取得します。

f:id:kenev:20190825100405p:plain
PERCY_TOKEN

これをCircleCIのProjectのSettings > BUILD SETTINGS > Environment Variablesから設定します。

f:id:kenev:20190825100554p:plain

CI実行

以上の設定が完了したらCircleCIのWorkflowを実行します。全部うまくいけばCIは成功!

f:id:kenev:20190825100640p:plain
CI成功

Percy検証

ここまでで設定が完了したので、いよいよPercyの機能を見てみます。

ベースデータの登録確認

現時点でのPercyのプロジェクトを覗いてみると:

f:id:kenev:20190825100856p:plain

何やら結果が増えているのがわかります。(1のFailは気にしないでください)

この中を見ると、Percyのレビュー画面に遷移します。

f:id:kenev:20190825100945p:plain
レビュー画面

それっぽい画面が表示されているのがわかります!ちょっと最初はわかりづらいかもしれませんが、左が比較元で、右が今回の変更点です。初めてPercyに情報を送ったので、左側には何もないのがわかります。

初回は全てApproveしちゃってOKなので、右上のApprove Allをクリックします。

f:id:kenev:20190825101258p:plain
Approve All

Percy上でもUnreviewedからApprovedになっているのがわかります。

f:id:kenev:20190825102906p:plain

これでベースとなるデータの登録ができたことになります。

画面に変更を加えて検証

ここからが本番!それでは、なんらかの拍子にピザの色をCSSで緑に変更してしまったとしましょう。

f:id:kenev:20190825103505p:plain
ピザの色を緑に変更

通常のe2eテストであればこのバグをキャッチするのは難しいです(不可能ではない)。しかし、Percyを利用していると変更前の画面と、変更後の画面の比較を自動で行ってくれるので、キャッチできるんです!

ということで上の変更内容をGitHubにPushしてPRを作ります。PercyとGitHubの連携も行っているおかげで、PRにPercyが表示されるのがわかります。

f:id:kenev:20190825103850p:plain

ここからPercyに飛ぶと!

f:id:kenev:20190825104128p:plain

なんだか想定と違う。。。?比較元はさっき用意したはずなのに無い。今更ですがそもそも「比較元はどうやって選んでいるのか?」という点が気になります。

公式ドキュメントに専用のページがあります。

docs.percy.io

確かに今回の検証はちょっと特殊で、sample-branchをベースにして、そこからsample-change-pizza-base-colorのbranchを生やしているので、比較元はsample-branch、比較先はsample-change-pizza-base-colorとなります。公式ドキュメントのPull request buildsに該当するから、特に意識せずに動作してほしいところですが、どうもそんなに単純な話じゃないみたいです(僕が設定を間違えている可能性が高いのでこれは別途調査が必要)。

明示的に比較元を設定することができるので、今回はCircleCIからPERCY_TARGET_BRANCH環境変数を設定してテストを再実行することにしました。

f:id:kenev:20190825105407p:plain

再度Percyの結果を見ると:

f:id:kenev:20190825105506p:plain

「おお!!」と言いたくなるようなdiffが生成されています。差分が赤くわかりやすく表示されています。そしてその部分をクリックすると、元の画像とのトグルもできるようになっているんです。

f:id:kenev:20190825105629g:plain

感動します!!

また、Slackにも随時ちゃんと通知が来ているのでSlackユーザーにとってもうれしい限り。

f:id:kenev:20190825110729p:plain

疑問点については指摘をして、修正してもらうorApproveをすることでレビューのプロセスを勧めていくことができます。

Percyをうまく活用することでかなりレビューの手間を省けるのと、品質のレベルを1ランク上げられるのではないかと思います。

まとめ

  • Percy + Cypressを試した
  • SaaSのおかげでVisual Regression Testのハードルがかなり下がったと感じた
  • 画面の表示に関連したレビューのプロセス(スピード)がかなり改善すると感じた

まだまだ氷山の一角

この記事で紹介できたのはまだまだPercyの力のほんの一部です。(僕もまだ触ったばかり)

例えばSDKであれば現時点(2019-08-25)で↓だけ項目が用意されています。

さらに、機能に関しては上で紹介している基本的なものに加えて

  • Ignore regions
    • CSSでPercy実行時だけ表示・非表示にする場所を制御できる
  • Responsive visual testing
    • Percy用のSnapshot取るときに幅を指定してレスポンシブデザインのチェックもできる
  • Cross-browser visual testing
  • Automatic diff matching
    • 同じ差分内容であればPercy側で勝手にグルーピングしてくれて、まとめてApproveできる

があります。

試してみたいことが山のようにありますが、この記事はここまでとします!

Percyを使ってガンガン恩恵を受けていきたい気持ちが高まりました。気になるのは料金体系ですが、どうやらこれはSnapshotの枚数で変わってくるみたいです。「月に何枚のSnapshotを撮るのか」というのはシステムのユースケースと、変更の量(PR)、クロスブラウザだったりレスポンシブをどこまでテストするかなどで変わってくるのでパッとは想像しにくいですね。人がそれをテストした場合のコストと比較してみると恩恵がわかりやすくなりそうです。

Percy以外にもSaaSがあると思いますので、それらとの比較も検証したいですね!

OPAのDecision Logsを使ってログを残す

Open Policy Agent (以下OPA)で様々なPolicyの判断をするにあたって、ログをどのように残すのかが気になってきました。

OPAを初めて聞いた、あるいはあまり知らない場合は以下の記事を参考にしていただければ!

kenfdev.hateblo.jp

ログを残す方法としてOPAではDecision Logsという仕組みがあるのがわかりました。

www.openpolicyagent.org

実際に使ってみたのでどんな感じなのかというのを紹介します。

TL;DR

この記事のサンプルコードのリポジトリを以下に置いてますので、docker-compose upでお試しできます!

github.com

Decision Logsの仕組み

Decision Logsの仕組みはシンプルで、OPAの設定ファイルで指定したLog ServiceにPOSTしてくれます。流れとしては下図の通り。

f:id:kenev:20190623083920p:plain

百聞は一見に如かずなので、実際に簡単なサンプルコードで見てみます!

OPAにはcurlでリクエストして、Log Serviceには超シンプルなNode.jsのサーバーを使います。

f:id:kenev:20190623111521p:plain

Policyの準備

まずはPolicyを作ります。policy.regoというファイルで、下のようなuser == "john"であればtrueになるallowというルールを定義しておきます。

# policy.rego
package foo

default allow = false
allow {
  input.user == "john"
}

Policyが正常に使えるか見てみます。まずはdockerコマンドで立ち上げ。

docker run -p 8181:8181 -v $(pwd):/workspace openpolicyagent/opa run --server /workspace/policy.rego

今回作ったPolicyはpackage foo配下のallowルールになるのでエンドポイントは/v1/data/foo/allowになります。curlで下のようにリクエストしてみると:

$ curl -XPOST -d '{ "input": { "user": "john" } }' http://localhost:8181/v1/data/foo/allow
{"result":true}

結果が{ "result": true }で返ってきているのが確認できます。

Mockサーバーの準備

ログを受け取れるように簡単なAPIサーバーをNode.jsで作ります。server.jsとして下のNode.jsサーバーを作ります。

// server.js
const express = require('express');
const bodyParser = require('body-parser')

const app = express();
const port = 3000;

app.use(bodyParser.json())

app.post('/logs', (request, response) => {
  console.log(JSON.stringify(request.body, null, 2));
  response.sendStatus(200);
});

app.listen(port, err => {
  if (err) {
    return console.log('something bad happened', err);
  }

  console.log(`server is listening on ${port}`);
});

これはポート3000で待ち受けてPOST /logsエンドポイントを公開しているサーバーになります。POST /logsを受け取るとログをconsole.logに出力して、レスポンスは200を返すようにしています。

OPAの設定

Logサービスはできたので、次にOPA側のDecision Logsの設定を加えてログが送信されるようにします。

まず、OPAには設定ファイルを--configオプションとして渡すことができます。以下、公式ドキュメントで詳しく記載されています。

www.openpolicyagent.org

設定ファイルはYAMLで書き、Decision Logsを有効化するには以下のような設定にします。

# config.yml
services:
  logger:
    url: http://logger:3000/

decision_logs:
  service: logger
  reporting:
    min_delay_seconds: 5
    max_delay_seconds: 10

OPAはdecision_logs.serviceに設定したサービスのエンドポイントに下の形でログを送信します。

POST /logs[/<partition_name>] HTTP/1.1
Content-Encoding: gzip
Content-Type: application/json

デフォルトで/logsになるのですが、/logs配下の別の場所にリクエストを飛ばしたければ設定ファイルのdecision_logs.partition_nameに値を設定します。

decision_logs.min_delay_seconds, decision_logs.max_delay_secondsが若干わかりづらいのですが、上の設定であれば、5秒毎にログを送信して、送信失敗した場合は送信間隔が最大10秒になるまではリトライしてくれるという内容になります。

delay_secondsの挙動についてはドキュメントだと曖昧だったのでコードで確認しました

検証

ではOPAとLog用のServiceが立ち上がるようにdocker-compose.ymlを作ります。

version: '3'

services:
  logger:
    build: ./logger
    image: logger
  opa:
    image: openpolicyagent/opa:0.13.0
    ports:
      - 8181:8181
    volumes:
      - .:/workspace
    command: ["run", "--server", "-c", "/workspace/config.yml", "/workspace/policy.rego"]

docker-compose upで起動します。

先程同様にOPAにリクエストしてみましょう!

curl -XPOST -d '{ "input": { "user": "john" } }' http://localhost:8181/v1/data/foo/allow

結果は以下のように返ってくるのがわかります。

{
  "decision_id":"79f24c2d-ce6a-4da1-8d2f-bd0b3c23a690",
  "result":true
}

docker-composeのログを見るとNode.jsのサーバーとOPAもログを出力しているのがわかります。

logger_1  | [
logger_1  |   {
logger_1  |     "labels": {
logger_1  |       "id": "abc47ac3-ab0a-4e4f-9ba2-316af7b2c9b4",
logger_1  |       "version": "0.13.0"
logger_1  |     },
logger_1  |     "decision_id": "79f24c2d-ce6a-4da1-8d2f-bd0b3c23a690",
logger_1  |     "path": "foo/allow",
logger_1  |     "input": {
logger_1  |       "user": "john"
logger_1  |     },
logger_1  |     "result": true,
logger_1  |     "requested_by": "172.18.0.1:59272",
logger_1  |     "timestamp": "2019-08-10T00:30:01.4419929Z",
logger_1  |     "metrics": {
logger_1  |       "timer_rego_module_compile_ns": 18600,
logger_1  |       "timer_rego_module_parse_ns": 32800,
logger_1  |       "timer_rego_query_compile_ns": 214200,
logger_1  |       "timer_rego_query_eval_ns": 90700,
logger_1  |       "timer_rego_query_parse_ns": 1098600,
logger_1  |       "timer_server_handler_ns": 1691000
logger_1  |     }
logger_1  |   }
logger_1  | ]
opa_1     | {"level":"info","msg":"Logs uploaded successfully.","plugin":"decision_logs","time":"2019-08-10T00:30:01Z"}

Decision Logが送信されているのがわかります!これでOPAがどのようなインプットでどのような判断を下したのかというログを残していくことができることがわかりました。

機密データのMasking

それでは、Decision Logsについてもう一歩先に進んでみます。

例えば以下のようなリクエストをOPAに投げたとします。

curl -XPOST -d '{ "input": { "user": "john", "password": "secret" } }' http://localhost:8181/v1/data/foo/allow

このときのDecision Logは以下のようになります。

logger_1  | [
logger_1  |   {
logger_1  |     "labels": {
logger_1  |       "id": "abc47ac3-ab0a-4e4f-9ba2-316af7b2c9b4",
logger_1  |       "version": "0.13.0"
logger_1  |     },
logger_1  |     "decision_id": "64475260-33ec-4d29-b375-afb61a2b2624",
logger_1  |     "path": "foo/allow",
logger_1  |     "input": {
logger_1  |       "password": "secret",
logger_1  |       "user": "john"
logger_1  |     },
logger_1  |     "result": true,
logger_1  |     "requested_by": "172.18.0.1:59276",
logger_1  |     "timestamp": "2019-08-10T00:35:55.2005304Z",
logger_1  |     "metrics": {
...
logger_1  |     }
logger_1  |   }
logger_1  | ]

おもいっきり"password": "secret"が出力されているのが確認できます。

"input": {
  "password": "secret",
  "user": "john"
}

このように機密性の高いデータがinputに含まれている場合に、それが平文でログに残されていくのは穏やかな状況ではないですよね。そういう情報を隠せるようにOPAのDecision LogsにはMaskする機能があります。

仕組みとしては比較的シンプルで、OPA自身がDecision Logを送信する前に(デフォルトでは)data.system.log.maskのルールを評価します。その結果に応じて情報を消します(Maskします)。これもさっそく実装してみます。

mask.regoというファイルを作って、中身を以下のようにします。

# mask.rego
package system.log

mask["/input/password"]

これはsystem.logパッケージのmaskルールを作ったことになり、OPA上のルールとしてはdata.system.log.maskに配置することになります。maskルールの中身(mask[<ここ>])にはJSON Pointerを記述することができて、そこに消したい(Maskしたい)情報へのパスを記述します。上の例であればinput.passwordの内容をログから消したいのでmask["/input/password"]と記述します。

あとはこのmask.regoを起動時に読み込んでおけば大丈夫です。docker-compose.ymlcommandを編集しておきます。

# docker-compose.yml
version: '3'

services:
# ...
  opa:
# ...
    command: ["run", "--server", "-c", "/workspace/config.yml", "/workspace/policy.rego", "/workspace/mask.rego"]

それではもう一度サービスを再起動(docker-compose down && docker-compose up)してみてリクエストしてみます!

curl -XPOST -d '{ "input": { "user": "john", "password": "secret" } }' http://localhost:8181/v1/data/foo/allow

Decision Logsを見るとinputからpasswordが消えているのがわかります!消されたものに関してはerasedの中にログが残るのも確認できます。

logger_1  | [
logger_1  |   {
logger_1  |     "labels": {
logger_1  |       "id": "734fe644-cfa9-4fed-9b87-dd11edbd4c8e",
logger_1  |       "version": "0.13.0"
logger_1  |     },
logger_1  |     "decision_id": "9bbf532b-b7f8-49b9-a8fd-2711c24ece70",
logger_1  |     "path": "foo/allow",
logger_1  |     "input": {
logger_1  |       "user": "john"
logger_1  |     },
logger_1  |     "result": true,
logger_1  |     "erased": [
logger_1  |       "/input/password"
logger_1  |     ],
logger_1  |     "requested_by": "172.18.0.1:54560",
logger_1  |     "timestamp": "2019-08-10T00:46:02.994297Z",
logger_1  |     "metrics": {
logger_1  |       "timer_rego_module_compile_ns": 18800,
logger_1  |       "timer_rego_module_parse_ns": 1723900,
logger_1  |       "timer_rego_query_compile_ns": 248100,
logger_1  |       "timer_rego_query_eval_ns": 206800,
logger_1  |       "timer_rego_query_parse_ns": 2582800,
logger_1  |       "timer_server_handler_ns": 5260600
logger_1  |     }
logger_1  |   }
logger_1  | ]

このように、Decision LogsのMaskingができることも確認できました。MaskのルールはRegoで書かれているので、ルールの中身を書くことでさらにMaskする条件を絞っていくこともできます。

例えば(こんなルールは無いでしょうけど)userjohnのときだけMaskしたい場合は、下のようなMaskルールを書くことができます。

package system.log

mask["/input/password"] {
  input.user == "john"
}

v0.13.0から使えるようになったConsole Decision Logger

この記事を書いている途中でOPAのv0.13.0がリリースされました!そしてその中でタイムリーにDecision LogsにConsole Decision Loggerという機能が追加されました。今までDecision Logsを見るには上のようにLoggerを別サービスで用意しておく必要がありましたが、v0.13.0からは標準出力してくれるようになりました!!これは開発時にはかなりうれしい機能になります。

さっそくこれも試してみます。2種類やり方があります。

config.ymlで指定

config.ymlは以下のようになります。(削除する場所がわかりやすいように元々の設定をコメントアウトしています)

# 必要なのはdecision_logs.console = trueのみ!

# services:
#   logger:
#     url: http://logger:3000/

decision_logs:
  console: true
  # service: logger
  # reporting:
  #   min_delay_seconds: 5
  #   max_delay_seconds: 10
起動引数で指定

OPAの起動引数に含めるやり方でも大丈夫です。

opa run --server --set decision_logs.console=true

今回の例であればdocker-compose.ymlを以下のように変えて、config.ymlを使わないようにすることもできます。

version: '3'
# ...
  opa:
    image: openpolicyagent/opa:0.13.0
 # ...
    command: ["run", "--server", "--set", "decision_logs.console=true", "/workspace/policy.rego", "/workspace/mask.rego"]

Console Decision Loggerを検証

では、再びサービスを再起動(docker-compose down && docker-compose up)してリクエストしてみます!

curl -XPOST -d '{ "input": { "user": "john", "password": "secret" } }' http://localhost:8181/v1/data/foo/allow

今回はLoggerサービスではなく、OPA自身のサービスに注目すると:

opa_1  | {"decision_id":"2bb5bd90-856b-4ba7-87db-d995b96d3cca","erased":["/input/password"],"input":{"user":"john"},"labels":{"id":"d54018d5-6700-437f-829e-6fa24210972c","version":"0.13.0"},"level":"info","metrics":{"timer_rego_module_compile_ns":17900,"timer_rego_module_parse_ns":18300,"timer_rego_query_compile_ns":209900,"timer_rego_query_eval_ns":78600,"timer_rego_query_parse_ns":823000,"timer_server_handler_ns":1331300},"msg":"Decision Log","path":"foo/allow","requested_by":"172.27.0.1:45836","result":true,"time":"2019-08-10T21:44:21Z","timestamp":"2019-08-10T21:44:21.378772Z"}

1行にまとめられてるのでちょっとみづらいですが、OPAのログにDecision Logが標準出力されているのがわかります!これはさり気なく結構うれしい新機能だと思います。開発時に積極的に使っていきたいです!

修正方法については以下PRにもしています。

github.com

まとめ

  • OPAではDecision Logs機能を使うことでどういうインプットでどういう判断をしたかログを残すことができる
  • Decision LogsにはMask機能があるのでログを残しておきたくない情報(パスワードなど)を消すことができる
  • v0.13.0からはConsole Decision Loggerが追加されて、Decision Logsが容易に見れるようになった

おまけ

JSON Pointerと配列に関するドキュメントの記述がちょっとわかりにくかったのでプチContributionしました!

github.com

KubeCon + CloudNativeCon Europe 2019の「Deep Dive: Open Policy Agent」を視聴して

しばらくOPA(Open Policy Agent)の講演を観ることができていなかったので、KubeCon CloudNativeCon Europe 2019の講演をYoutubeで視聴しました。

www.youtube.com

構成

時間 内容
1:27 Example: Application
5:38 Example: Kubernetes Platform
7:14 OPA: Unified Policy Enforcement Across the Stack
8:40 OPA: General-purpose Policy Engine
11:25 Community Growth
12:53 Community Highlights: Configuration Guardrails
15:30 Community Highlights: Chef Automate IAM
16:42 Community Highlights: RPG Engine
17:24 Recent Developments
17:27 play.openpolicyagent.org
20:45 v0.11: Improved Debugging: notes trace filter
22:04 v0.11: Language Improvements: some keyword
23:20 v0.11: Native Integrations: WebAssembly progress
25:25 Looking forward
25:33 Google Summer of Code: IPTables integration
26:43 Use Case: Application & End-user Authorization
30:09 Help Us Improve Discoverability
31:31 Q&A

OPA: Unified Policy Enforcement Across the Stack

OPAは様々な場面においてPolicyを適用できると述べています。以下はそれぞれ紹介されている場面と、関連するサービスや技術スタックです。

  • Admission Control
  • Container Execution, SSH, sudo
  • Microservice APIs
    • e.g. Istio, Linkerd, spring, Kong, envoy
  • Risk Management
    • e.g. Terraform, Forceti Security
  • Data Protection & Data Filtering
    • e.g. ceph, kafka, MinIO, SQLite, elastic

OPA: General-purpose Policy Engine

OPAの概要について述べています。OPAの概要については僕も記事を書いてますのでざっくりどういうものか知りたい方はぜひ!

kenfdev.hateblo.jp

講演の中では以下の話が特に重要だと感じます。

Decouple "Policy Decision Making" from "Policy Enforcement"

APIを実装する場合、何も考えずに実装していくとPolicyに関わる部分も自然とビジネスロジックの中に書いてしまったり、データベースに情報を取得しにいく部分で書いてしまいがちです。Policyによる判断(Decision)をする場所と、判断に基づいた処理(Enforcement)を行う場所を分ける(Decouple)ということです。

Enforcement is the Service's job. Making the decision is OPA's job.

判断(Decision)はOPAがして、判断に基づいた処理(Enforcement)はサービス側で、というマインドが大事になりそうですね!

Community Highlights: Chef Automate IAM

Chef Automate IAMのユースケースを紹介しています。Chef Automateは内部的にOPAをPolicy Engineとして使っていて、OPAをライブラリとして使う例としてもとても勉強になるOSSです。また「Well-documented architecture」と述べられているように、以下のドキュメントがとてもわかりやすくOPAを使った認可の仕組みを説明してくれています。

github.com

中でも、「Introspection (Who Can Do What?)」については、フロントエンドにどのように「誰」「何をできる」かを伝えられるか、というフローについても詳しく説明されていて非常に勉強になります。

play.openpolicyagent.org

f:id:kenev:20190808203446p:plain

Rego Playgroundの紹介がされました。

play.openpolicyagent.org

サクッとPolicyを試すのに便利ですし、書いたPolicyをシェアできるのも便利です!OPAのSlack上でもコミュニケーション手段としてよく使われています。

Use Case: Application & End-user Authorization

認可に対するよくある疑問点とOPA側からの回答が述べられていました。その回答には今OPAが何をできるか、と今後どうしていこうとしているかについて述べられています。

以下については僕も今後かなり期待したいところです。

How do you delegate control to your end-users?

「OPAでIAMのようなことをしたい」という質問はよくSlackでも見かけます。これはエンドユーザーがPolicyの作成や管理ができるユースケースになります。Chef Automate IAMがリファレンスとしてよく紹介されるのですが、今後このようなモデルをOPAから公式に提供できるようにしてくれるそうです。

どのような仕組みで提供されるのか楽しみですね!

How do you leverage context, e.g. HR DB?

大規模なアプリケーションであればOPAに、判断に必要なデータ(context)を全部(インメモリで)あらかじめ読み込ませておくのは現実的じゃないです。今後「Data-fetching」が判断(Decision)のタイミングで行えるような機能が増える予定のようです。(XACMLで言うPIPの機能が強化されるイメージだと僕は捉えています)

現状では僕の知っている限りはOPAをライブラリとして使って、自前のサービスを作るか、OPAのビルトインであるhttp.sendを使って外部にAPIリクエストを行う方法しか無いはずです。この機能が実現するとOPAを使うハードルがぐっと下がりそうな気がします。

How do you render UIs based on policy?

フロントエンドでPolicyに基づいてボタンを表示するかしないかを判断できるようにするためにWASM(WebAssembly)が使えるようになるようです。

これが実現すると、上で述べたChef Automate IAMで提供されているようなIntrospection APIを作る必要が無くなるのではと思います。

まとめ

  • 「Deep Dive: Open Policy Agent」を視聴した
  • OPAをがっつり使っている例としてChef Automate IAMがかなり勉強になる
  • Policyの判断のタイミングで必要なデータをさらに取得する機能は今後に期待
  • WASMが実現すれば、フロントエンドのJSと連携してSPAでもPolicyを適用できそう
Slack

OPAの最新情報や質問は今の所Slackが一番収集しやすいと思います!特に「general」と「openpolicyagent」のチャンネルがおすすめです。

slack.openpolicyagent.org

stackoverflow

これから徐々にQAはstackoverflowに増えていくはずなので、以下のタグをチェックしておくとナレッジがたまりそうです。

stackoverflow.com

おまけ

公式ドキュメントの検索がしやすくなるようにAlgoliaのdocsearchが採用されそうです。ドキュメントの検索が無いというのがOPAのハードルを上げている一つの要因になっている気もするので、これはぜひとも早めにマージにもっていきたいです!

github.com