認可とは、特定のリソースへのアクセスを検証するプロセスです。最新のセキュリティワークフローを使ったシステムでは、ユーザーが何をする権限があるかを、アプリケーションがコントロールできます。このプロセスがアクセスコントロールです。
この記事ではアクセスコントロールについて、また、誰が何にアクセスできるかを決定するための構造とルールを提供するアクセスコントロールモデルを使う理由について学びます。
さまざまなアクセスコントロールモデルを理解できるよう、経費精算システムのようなアプリケーションの例を考えてみましょう。このようなタイプのシステムでは、あるユーザーが他のユーザーから提出された経費精算書を承認できるようにする必要があります。
このプロジェクトのMVPに取り組み始め、製品が進化し成長するにつれて、イテレーションさせたいと考えているとします。私自身はRuby on Railsに精通しているため、そちらを使って構築することにしました。新しい経費精算アプリを開発し、自分ではそれを誇りに思っています。経費精算書の作成、精算書の更新と削除、一覧表示と承認など、誰もが必要とする機能はすべて備えています。次に、誰が何をできるかを制限しましょう。そのために認可システムを構築します。
最初のステップ: 認証
他人が自分のアプリにアクセスして自分の経費精算書を作成したり、既存の精算書を閲覧できるようにはしたくありません。アプリへのアクセスを、許可されたユーザーに制限する必要があります。この許可されたユーザーとは自分のアプリが認証済みであると認識しているユーザーです。
あるユーザーが有効な認証情報を提供した場合(つまり認証された場合)、アプリへのアクセスが許可されます。
よく、認証と認可という用語を混同する人がいますが、その主な理由は、認証は通常、認可の最初のステップだからです。
認証とは、その人が本人であることを検証するプロセスであり、認可とは、その人がアクセスできるものとできないものを検証するプロセスです。
非常にシンプルなユースケースの場合のみ、認証だけでユーザーがアプリケーションを使用することを認可できます。例えば、経費精算アプリの場合、ユーザーを認可する条件として認証だけを使用することは潜在的な問題につながります。認証されたユーザーならだれでも、報告書を作成したり、一覧化したり、承認したりできるようになるからです。おそらく、これは自分が望んでいることとは異なるでしょう。
では、どうすれば 一部 のユーザーだけが報告書の操作を行えるようになるのでしょうか?🤔
ロール(役割)の使用
ユーザーは経費精算書を提出できますが、この時点で、経費精算書の承認を担当するユーザーグループが必要になります。「経費承認者」のロール(役割)の定義もすることになるでしょう。
権限モデルを定義しているときに、共有する責任に基づいてユーザーをグループ化できることに気づいた場合は、ロールベースアクセスコントロール(RBAC)を参照しましょう。 このモデルはアプリがアクセス管理としてユーザー特有の責任、すなわち役割に基づいてユーザーができること、できないことを決定できるシンプルで管理しやすいアプローチを提供します。
Auth0 by Okta (以下、Auth0)では、ロールがリソース上で実行できる操作の集合体であると定義されます。今回の場合、経費精算書 の「承認」を実行する「経費承認者」ロールを作成してユースケースを実現できます。すばらしいですね!🙌
現時点で、ユーザーは経費精算書を提出し、経費承認者は承認が可能になっています。しかし、どのユーザーがどの精算書を閲覧できるかをまだ定義していないため、全てのユーザーが他のユーザーの精算書を閲覧できてしまいます。😩
権限の使用
アクセスコントロールの実装において、ロールには限界があります。アプリケーションが成長すればするほど、可能なロールのリストを維持することが複雑になります。RBACを使用するだけでは、ロールが爆発的に増える可能性があり、何百ものロールを管理することになってしまいます。そのため、権限を併せて導入することにしました。
権限とは、リソースに対して実行できるアクションの宣言です。Auth0でAPIに対して定義された権限は、アプリケーションで使用される、アクセスコントロールメカニズムの一部として使用できます。
権限を使用すると、アプリケーションロジックはアクセスコントロールをロールだけに依存する必要がなくなります。より詳細なアクセスコントロールを定義でき、しかもAuth0では、ユーザーに権限を一括で割り当てる便利な方法として、ロールに権限を追加できます。
この場合、ユーザーが経費承認者または経費精算書の作成者である場合にのみ、精算書の閲覧を許可する権限を与えることになります。権限を実装するには、ユーザーがどの精算書を閲覧できるかを定義するために、いくつかのロジックをアプリケーションに追加する必要があります。これを経費承認者とそれ以外のユーザーの2つのグループに分けて考えてみましょう。
経費承認者はすべての経費精算書を閲覧でき、それ以外のユーザーは自分の精算書だけを閲覧できるようにするのが理想です。経費承認者のユースケースでは、例えば「経費精算書を読む」権限を彼らのロールに追加し、すべての経費承認者がすべての経費精算書を閲覧できることを、アプリケーションロジックで定義します。これまでのところ、とてもうまくいっています。このユースケースでは、経費承認者のロールで十分です。🙌
それ以外のユーザーについて、自分自身の経費精算書だけを閲覧できるようにするには、「経費精算書を読む」権限が必要です。これらのユーザーは「経費承認者」のロールを持っていないので、アプリケーションロジックで自分自身が提出した精算書以外を読めないように設定できます。ロールと権限を併用すると、ユーザーが閲覧可能な精算書はどれかということをアプリケーションが決定できます。
今回の場合、ユーザーが経費承認者のロールを持っているかどうかを検証するメソッドを設けることができます。そうでない場合は、ユーザーが経費報告書の提出者であるかどうかを、リレーショナルデータベースで照合します。Rubyのような構文で訳すと次のようになります。
def can_read_expense_report(user)
return true if user.expense_approver?
expenses = db.fetch(
"SELECT expenses.id, users.user_id
FROM expenses WHERE id = %
JOIN users WHERE users.user_id = expenses.submitter_id")
authorize!(user, :read, :expenses)
end
他には、CanCanCanやpunditのような外部gemを使うというオプションもあります。実装の細かい点は、完全に自分自身と自分のアプリケーション次第です。
この時点で、各ユーザーは自分が提出した経費精算書を確認でき、また、すべての精算書の閲覧と承認を可能にする経費承認者ロールが存在することになります。関係者にアプリケーションを見せると、彼らは、経費承認者は自身の直属の部下が提出した経費のみを精算できると決定しました。😧
息を深く吸って、🧘もう一度この決定に対して考えてみましょう。
属性を使ったアクセスコントロール
ユーザーエンティティはmanager_id
属性を持っているので、システムでは、ユーザーの直属の部下を把握できると想定します。
属性(または特性)を評価する権限システムを定義する場合、属性ベースのアクセスコントロール(ABAC)を使用します。 ABACの目的は未承認のユーザーやアクション、つまり組織のセキュリティポリシーに「承認された」特性を持たないものからオブジェクトを守ることです。
セキュリティポリシーは次のようになります。
ユーザーAがユーザーBのマネージャーである場合、ユーザーAはユーザーBからの経費精算書を承認することができる
アプリケーションコードの中ではこのポリシーを実行するメソッドを定義しなければなりません。ここでは仮に、この情報がリレーショナルデータベースに存在しているとします。また、ログインしたユーザーがある別ユーザーの経費精算書を取得するためにクエリを実行します。その際に、もし、ログインユーザーが経費精算書を提出したユーザーのマネージャーであれば、経費を承認できるとしましょう。これをRuby的な(というより疑似コード的な)構文で行う方法は、下記のようになります。
def approve_expense(user)
expense = db.fetch(
"SELECT expenses.*, users.manager_id as submitter_manager_id
FROM expenses WHERE id = %
JOIN users WHERE users.user_id = expenses.submitter_id")
authorize!(user, :approve, :expense)
end
上記のコードは権限を実装したときに書いたものと似ています。重要な違いは、approve_expense
メソッドとポリシーがオブジェクト(この場合はユーザー)の属性に基づいていることです。ご覧のように、ABAC、RBAC、権限は同時に使用できます。💡
approve_expense
メソッドは、アプリケーションで非常にうまく機能します。🥳そのため、それを最適化し、スケーラブルにする方法を考えることでしょう。管理段階が1つしかない小さな組織では、この承認システムはうまく機能するかもしれません。しかし、管理が複数レベルに渡っている大きな組織になると、そのソリューションがパフォーマンスとメンテナンスの問題を引き起こす可能性があります。🤔
上記のコードは、認可ロジックをアプリケーションコードに埋め込んでいるため、ソースコードを見なければ、認可がシステムでどのように実装されているかを理解することが難しくなっています。
また、認可ポリシーに変更があれば、アプリケーションコードを変更する必要があります。
最後に、ユーザーが経費精算書を閲覧できるかどうかを承認または検証する必要があるたびに、運用データベースに負荷がかかり、リクエストに待ち時間が発生します。
今回のような経費承認者、提出者、経費精算書の間には明確な関係があります。管理者という属性は機能しますが、必要のない複雑さが加わるため、いったん導入を考え直すこともあるでしょう。
関係性に基づいたアクセス制御の必要性
ユーザーの属性と、アクセスしたいオブジェクトに基づいて認可システムを構成するのは、正しい方向でした。しかし、経営陣が考えを変え、アプリケーションを成長させる必要が出てきたとき、その解決策はあまりうまく機能しませんでした。🙄そこで、属性を使用する代わりに、ユーザー同士の関係や経費精算書に基づいて作業する方が理にかなっていると判断した場合、それを可能にする認可モデルが存在します。
リレーションシップに基づくアクセスコントロール(ReBAC)は、ユーザーとオブジェクトの関係や、オブジェクトと他のオブジェクトの関係に基づいてルールを表現できます。 この仕組みを用いて非常に複雑なコンテキストを記述できる表現力の豊かな認可モデルを提供します。この認可モデルがうまく機能する例として、ソーシャルメディア(SNS)アプリがあります。SNSではユーザーが他のユーザーとの関係に基づいて、自分のプロフィールや情報にアクセスできる人をコントロールできます。
ReBACは、最も柔軟性を提供してくれるモデルの一例です。高い柔軟性を持つモデルは、よくFGA(Fine-Graned Authorization)という用語で呼ばれます。規模が大きくなると、システムには何百万ものオブジェクト、ユーザー、リレーションシップが存在することになり、オブジェクトは定期的に追加され、アクセス許可は常に更新されます。例えば、Google Driveでは、ドキュメントやフォルダーへのアクセス権をユーザー個人またはグループのいずれかに付与できます。さらに、新しい文書が作成され、社内外を問わず特定のユーザーと共有されると、アクセス許可は定期的に変更されます。
きめ細かな認可モデルは、まさに自分のユースケースに必要なものだとお考えでしょう🤩。しかし、待ってください。もちろん、こういういものを構築するのは複雑ですよね? そのとおりです。嬉しいことに、我々がすでに構築しているものがあります。Okta FGAは簡単な解決策を提供し、以下のような方法で認可モデルとユーザーとレポート(経費精算書)の関係を定義します。
type report
relations
define approver: can_manage from submitter
define submitter: [user]
type user
relations
define can_manage: manager or can_manage from manager
define manager: [user]
このモデルでは、can_manage
という関係を使用し、ユーザー間の manager
関係を示しています。さらに、レポート(経費精算書)の承認を行えるのは提出者のマネージャーだけであることを示しています。
あるユーザーが管理階層において、他のユーザーのマネージャーであるかどうかをチェックしたい場合、Okta FGA Check APIを使用できます。Rubyでは、以下のようになります。
require "uri"
require "json"
require "net/http"
FGA_API_URL="YOUR_FGA_API_URL"
FGA_BEARER_TOKEN="YOUR_BEARER_TOKEN"
FGA_STORE_ID="YOUR_STORE_ID"
url = URI("#{FGA_API_URL}/stores/#{FGA_STORE_ID}/check")
http = Net::HTTP.new(url.host, url.port);
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = "Bearer #{FGA_BEARER_TOKEN}"
request["content-type"] = "application/json"
request.body = JSON.dump({
"tuple_key": {
"user": "user:alex",
"relation": "can_manage",
"object": "user:sam"
}
})
response = http.request(request)
puts response.read_body
# Response: {"allowed":true,"resolution":""}
認可システムの管理にOkta FGAのような外部のサービスを使用すると、アプリケーションコードから認可ロジックを削除でき、すべてのポリシーが一か所に集約できます。このため、ポリシーの保守が容易になります。また、Okta FGAは、大規模や個々のユースケースに対応でき、高い信頼性と低いレイテンシー提供を提供できるように最適化されているため、リクエストの待ち時間を最小限に抑えられます。例えば、今回のシナリオではユーザーが経費精算書を承認できるかどうかをチェックするたびにデータベースへアクセスする必要がなくなるため、レイテンシーを軽減できます。
認可モデルの導入を完了し、進捗状況を報告すると関係者は大喜びします!🥳 自分の進歩を皆が気に入ってくれます。そして、今度は同じ申請書の複数のバージョンやブランチ、チームを持つことができるgitのようなシステムを要求してきます。そこで、あなたはまた深呼吸🧘をして、締め切りがまた遅れることになると説明し、このドラマは続くのです…
まとめ
認可モデルは、アプリケーションで誰が何にアクセスできるかを決定するための構造とルールを提供します。ユースケースによっては、あるモデルが他のモデルよりも優れている場合もあります。あるいは、ユースケースの複雑さに応じて、複数のモデルを使うことになります。
システムでは認証が必要ですが、ユーザーがリソースにアクセスできるかどうかを確認する場合は認証だけでは不十分なことがほとんどです。ロールベースのアクセスコントロール(RBAC)モデルは、いくつかのロールで要件を満たすような単純なアプリケーションでは十分ですが、より細かなアクセス制御コントロールモデルを実現する場合は、ロールに関係なくユーザーへ権限を割り当てることを検討してもよいでしょう。例えば、ユーザーの属性に基づいてアクセスコントロールを定義できるABACを実装する、あるいはオブジェクトの関係に基づいてリソースへのアクセスを制限ReBACを実装できます。どのような要件であれ、Auth0とOkta FGAは、大規模な認可のニーズに対して、段階的に対応できるソリューションを提供します。