🎮

このユーザーはこの機能が使える、みたいなあれをどう実現するか

2020/12/10に公開
4

このユーザーはこの機能が使える、みたいなあれをどう実現するか

新しい記事公開しました!
https://zenn.dev/dove/articles/8bed47a7a839ad

結論

DBで実現するかアプリケーション側でライブラリで実現する。casbinが強そうだぞ。

https://casbin.org/en/

はじめに

ユーザーテーブルにisAdminというカラムを使って、アドミンかそうでないかで、アクセスコントロールを分けていたが、サービスが大きくなり、もう少し細かくコントロールする必要になった。そこで、情報収集してえた見識をまとめておく。

アクセスコントロールという考え方を知る

だれがこのファイルを操作できるのかみたいなやつ。おおきく4つのパターンがあるみたい。

  1. DAC
  2. RBAC
  3. MAC
  4. ABAC

DACとRBACとMAC

この3つはまとめて図で理解したほうが覚えやすいと思う。
DACとRBACとMACの図

DAC(任意アクセス制御)はユーザー側が任意にこの機能を誰が使えるか設定する。例えばgoogleでファイル共有するときに誰までが閲覧可能かどうか設定する、あれのこと。ちなみに、Linuxのファイルシステムなどに利用される。反対にMAC(強制アクセス制御)は完全にシステムからレベルごとに使える機能を限定されている。軍隊などでよく利用される。例えば、めちゃくちゃ下っ端だとここまでの情報しか取得できないみたいなやつ。DACはユーザーに自由度を与え過ぎるのが欠点。MACは、純粋にレベルがあがると利用できる機能が増えていくモデルなので、例えば作成はできるけど閲覧はできないみたいな表現ができないのが欠点。

RBAC(ロールベースアクセス制御)は2つに比べたら比較的新し目。ロールという中間層を設ける。ユーザー側からしたら割り振られたロールが可能な機能しか使えない。システム側からしたらユーザーに自由度は与えずに、かつ機能のわりふりが比較的表現度たかく行える。

ABAC

ABACの図

ちょっとトリッキー。最終進化系かもしれない。こちらは条件を複雑にしたもの。例えば、

  • 本店勤務である、店長クラスであれば削除ができる(閲覧と作成と更新もできる)
  • 店長クラスであれば閲覧と作成と更新ができる
  • 支店勤務であれば閲覧ができる
    みたいに、その人の属性によって細かく権限を変える考え方。めっちゃ複雑。もう少しシンプルにしてRBACでも対応できると思う。
  • 本店の店長ロールは全権限がある
  • 支店店長ロールは閲覧と作成と更新ができる。
  • 支店バイトロールは閲覧しかできない。
    みたいな感じ。

DACとRBACとMACをアプリの権限管理に活かして考えてみる

上記はどちらかというとシステムレベルでコンテンツにアクセスするさいのふるい分けでよく言われる話で、webサービスの権限的な部分での話とは少し違う気がする(ユーザーが自由に権限管理できたら権限の意味がない)。ただ上記の図は権限管理に活かせそうな気がする。ここは上記の図をヒントに、少し違った考えをしてみる。

下記はDBを利用しているが、動的に変化することがなければテキストファイル(yml的なものとか)でも良いと思う。

ユーザーidに直接できることを紐付けるパターン

DBで次のように権限テーブルを作成する。

userId view create edit delete
1 true true true true
2 true false false false

アプリケーション側ではユーザー取得に権限テーブルをjoinし、各操作の真偽値を保持しておく。

const create = () => {
  // さすがにuser.createという取得は嫌なので
  if(!user.create){
    throw new Error('create is not permitted.');
  }
  
  // create something.
} 

ロールにできることを紐付けるパターン

DBではuserテーブルにroleカラムができ、権限テーブルにroleIdと各操作がひも付く。
userテーブル

userId name roleId
1 bob 1
2 alice 2

権限テーブル

roleId name view create edit delete
1 ADMIN true true true true
2 GUEST true false false false

アプリケーション側のコード

const create = () => {
  if(!user.role.create){
    throw new Error('create is not permitted.');
  }
  
  // create something.
} 

もしくは、userテーブルにroleカラムだけおいておき、権限テーブルは作らないという選択もある。その場合はアプリケーション側で分岐する。

const create = () => {
  // 対応ロールが増えるとここの分岐が増える。
  // const permissions = ['ADMIN', 'LEADER']として、if(permissions.includes(user.role)) とするのもあり。
  if(user.role !== 'ADMIN){
    throw new Error(`role need admin. you role: ${user.role}`);
  }
  
  // create something.
} 

もしくはRBACを実現できるライブラリを導入して、権限テーブルの部分をアプリケーションがわのどこかであらかじめ宣言しておくという方法もある。

ユーザーにレベルをつけるパターン

ユーザーテーブルにレベルカラムをつけて、アプリケーション側でレベルが閾値以上かチェックする。どのレベル権限でどの操作ができるのかという情報はドキュメントにちゃんとまとめておかないと、あとで地獄をみそう。

userテーブル

userId name level
1 bob 2
2 alice 1

アプリケーション側のコード

const create = () => {
  if(!user.level <= 1){
    throw new Error('create is not permitted.');
  }
  
  // create something.
} 

specification パターン

デザインパターンのひとつにspecificationパターンというものがある。DDD本のどこかででてくる。複雑な条件のビジネスロジックを扱うパターン。これを利用する手もあるかなと思った。

おわりに

この分野初心者なので、もし詳しい方いましたら教えていただけるとありがたいです^^

Discussion

にゃーんにゃーん

僕なら『ユーザーにレベルをつけるパターン』にします!

『ユーザーidに直接できることを紐付けるパターン』だと、機能が増えるたびにDBをいじる必要がある や この権限変えた方がいいよねというときに既存のレコードを書き直すDB処理をする必要がある などの辛さが出てきます。

『ユーザーにレベルをつけるパターン』にしておき、ユーザーのクラスなどで権限を管理するのは一つの手です。user.can_view? や user.can_delete? などのメソッドを用意しておいて、レベルの値を意識することなく処理できるようにするなど

こうしておけばドキュメントを作らなくてもそのクラスを見れば、どのレベルがどの操作を許されているかが分かるような、、、

あくまでも一意見ですが、、

ハトすけハトすけ

ネココニャンさん、コメントありがとうございます^^

機能が増えるたびにDBをいじる必要がある

ここ確かにそうですね😳 特にどんどん機能を追加していくフェーズだと、度重なるスキーマ変更が開発の邪魔になりそうです。

ユーザーサイドでめちゃくちゃ細かい権限の動的変更が求められない限り、DBで管理するよりも、DBには識別(レベルやロールなど)だけ置いておき、アプリケーション側で対応するほうがよさげです😁

また、ユーザークラスにメソッドごとの使用許可メソッドを作っておき、そのメソッドを呼び出すまえに、必ず使用許可メソッドを契約的に呼び出して権限チェックをする方式、いいですね! こうすれば権限チェックコードがあちこちに散らばらずにコード自体をドキュメントとして使えそうです。

その方針でいこうかなと思います。ありがとうございます😊

スースー
  • ロールにできることを紐付けるパターン
  • ユーザーにレベルをつけるパターン

の下のuserテーブルの表記が崩れているかもです!
下のように書き直せばいけそうですー!
プルリクエストが出せないようなので、コメントにて失礼しますー

userId name roleId
1 bob 1
2 alice 2