じぶん対策

日々学んだことをアウトプットして備忘録にしています。

システムの権限方式について

はじめに

今回はシステムのアクセス制御、つまりユーザーの操作権限の管理のしくみを考えていきます。

管理者フラグ形式

最も簡単に実装できる方法です。
ユーザーテーブルに管理者フラグを実装し、管理者の場合は権限を持つように実装します。

  • メリットはシンプルな実装となること。
  • デメリットはユーザーごとに権限を設定できないこと。

「このユーザーにはこの操作のみを許可したい」のようなパターンに対応できません。管理者フラグが立っている場合はすべての権限を与えることになります。
また、以降紹介するテーブル設計パターンへの以降が難しくなります。(1から作り直すことになる)

ACL(Access Control List)

管理者フラグと比べてユーザーに個別に操作権限を付与できる方式です。
作成するテーブルとしては以下の3つを作成します。

  • ユーザーテーブル
    • ユーザーID
    • ユーザー名
  • ユーザー操作権限テーブル(中間テーブル)
    • ユーザーID
    • 操作権限ID
    • 権限の有無
  • 操作権限テーブル
    • 操作権限ID
    • 権限名

このパターンのデメリットは、権限の数、もしくはユーザー数が増えてくると管理が煩雑になる点です。
ユーザーと操作権限の中間テーブルにてすべて管理されているので、単純にユーザー数 * 操作権限数だけレコードが必要になります。
権限、もしくは権限数を追加する場合に対応する数だけレコードを新規追加する必要があるため、設計画面も煩雑になります。(ユーザーを追加する度にすべての機能について権限の設定を行わないといけない)

ACL形式を適用するパターンは、機能数、ユーザー数がどちらも少ないことが想定される場合です。
大抵のシステムの場合はどちらかがある程度の数となることが想定されると思います。
次に紹介するRBAC、ABACと比較してどれが適切な設計か検討しましょう。

RBAC(Role Based Access Control)

RBACは、ユーザーにロール(一般ユーザー・管理者など)が紐づき、ロールに対して権限が紐づきます。
実装の難易度と解消できる複雑さのバランスが取れていて、権限の管理について考える場合はまず検討するべき考え方になるかと思います。

作成するテーブルは以下のようになります。

  • ユーザーテーブル
    • ユーザーID
    • ユーザー名
  • ユーザーロールテーブル(中間テーブル)
    • ユーザーID
    • ロールID
  • ロールテーブル
    • ロールID
    • ロール名
  • ロール操作権限テーブル(中間テーブル)
    • ロールID
    • 操作権限ID
  • 操作権限テーブル
    • 操作権限ID
    • 権限名

上記テーブル構成を見るとわかるようにACLに比べるとテーブル設計としては少し複雑になります。

比較する際にはユーザー側、実装側のそれぞれの視点からどういう操作が必要になるか考えましょう。

ACL形式でユーザーを追加する場合のユーザー側の操作

ACL形式で権限が管理されている場合、ユーザーに直接権限が紐づきます。
そのため、ユーザーを新規追加する場合は以下のような流れになります。

  1. ユーザーに必要な情報を入力(名前等)
  2. ユーザーに紐づく権限をひとつひとつ編集して設定

DB設計を見直すと、ユーザーを1人追加する度に機能の数だけレコードを中間テーブルに追加する必要があります。
すべての機能について、そのユーザーの権限を考えて登録する必要があります。

RBAC形式でユーザーを追加する場合のユーザー側の操作

RBAC形式で権限が管理されている場合は、

  • ユーザーとロールが紐づく
  • それぞれのロールと権限が紐づく

の2つの関連が発生します。

ユーザーを新規追加する場合の操作は、

  1. ユーザーに必要な情報を入力(名前等)
  2. ユーザーに紐づくロールを一つ設定

という流れになります。
ACL形式と比較すると、ユーザー新規追加の際にはロールを一つ設定するのみで、各権限については個別に設定せずに作成が可能になります。
管理の面から考えても権限はロールに紐づくため、ユーザーの数だけ設定が必要なACL形式と比較するとロールの数に絞られ、いくらか簡単に管理ができます。

ACL形式で機能を追加する場合の実装の流れ

ACL形式の場合に機能を追加した場合を考えてみます。
基本的に機能の追加に伴う権限の更新についてはシステム側でマイグレーション等を行うことになると思います。

  1. 機能を追加
  2. 機能の操作権限とユーザーIDの紐付けを中間テーブルに登録

ユーザーの数だけレコードを中間テーブルに追加する必要があります。
また、新規追加された機能の権限をユーザーごとに考えなければならず、かなり複雑になります。

RBAC形式で機能を追加する場合の実装の流れ

RBAC形式で権限が管理されていて機能を追加する場合、ユーザーと各権限には直接関連が発生しないことになります。

  1. 機能を追加
  2. 機能の操作権限とロールIDの紐付けを中間テーブルに登録

中間テーブルに追加するレコードの数はロールの数になります。
また、新規追加された機能の権限について考えるのもロールの数になるため、ACL形式と比較すると少なくなります。

ACL形式とRBAC形式の比較結果

比較結果として大事なのはユーザー側からみた操作がシンプルになるのはどちらかという観点です。
複数の権限を一つのロールに紐付けることができるのでRBACのほうがシンプルになります。

ただ、実装としてはテーブル数等の面で複雑になりますが、機能を追加する際に追加するレコード数は少なくなるため、RBACを選択するほうがメリットが大きそうです。

前提として、追加する機能の数や想定されるユーザー数を考慮した上で判断しましょう。

ABAC(Attribute Based Access Control)

先程紹介したACL、RBACの他にユーザーの属性をベースに考えるABAC方式があります。

たとえば、ユーザーの「部署」「役職」といった属性をチェックし、部署が「人事部」、役職が「部長」の場合のみアクセスを許可するといった制御が必要な場合はABAC形式を検討します。

管理方式の比較まとめ

ACL RBAC ABAC
長所 実装自体は楽 実装は楽。ロールの数が少ない場合は管理コストが低い きめ細かい権限管理が可能。新しくユーザーを追加する場合でも既存のルールを変更する必要がない。
短所 管理が煩雑 きめ細かい権限管理のためにはロールの数を増やす必要がある 実装が一番複雑。慎重に設計を検討する必要がある

RBACの実装を考えてみる

個人的には大規模なシステム出ない場合はRBACをベースに考えるのが一番汎用性が高いかなと思いました。
RBACの際の実装パターンとして、DB上のデータとして管理するか実装上で直接制御するかの二通りが考えられます。

まずは実装上で直接制御する場合の処理について考えてみます。

クリーンアーキテクチャを意識しながら実装するとRoleというエンティティを用意するのが自然かなと思いました。
また、それらのインターフェースを用意しておくことでユースケースから具体的なRoleクラスの実装を意識せずにメソッドを使用できます。

RoleInterface

interface RoleInterface
{
    public function id(): int;
    public function name(): string;
}

AdminRole(管理者ロール)

class AdminRole implements RoleInterface
{
    /**
     * @var string
     */
    private string $name;

    private const ID = 1;
    private const NOT_ALLOWED = 0;
    private const READ_ONLY = 1;
    private const ALLOW_UPDATE = 2;

    public function __construct()
    {
        $this->name = 'admin';
    }

    /**
     * @return int
     */
    public function id(): int
    {
        return self::ID;
    }

    /**
     * @return string
     */
    public function name(): string
    {
        return $this->name;
    }

    /**
     * @return int
     */
    public function FunctionAuthority(): int
    {
        return self::READ_ONLY;
    }

上記のような形になるかと思います。あとはビジネスロジックに応じたバリデーションをValueObjectを用意して凝集しておけば実現できそうです。

ここで意識しておきたいことはRBACの場合はロールの数が増えた場合に実装でのカバーが難しくなることです。
そのためできるだけDBへの移行がしやすいような設計にしておいたほうがいいです。

具体的にはRepositoryパターンを採用しその中に隠蔽します。
DBから取得する場合はクエリビルダ等を使用しますが、クラスで制御する場合は具象クラスを返すロジックをRepositoryに隠蔽しておきます。具体的にはIDを入力をとして渡して動的に具象クラスを生成する等の方法が考えられます。

このようにしておけばDBへの移行が必要となった場合も比較的スムーズな移行が可能です。

所感

今回は権限管理について整理してみました。
ひとえに実行する権限を管理するといっても様々な方法がありそれぞれのパターンで実際の運用上の操作等を想定することが大切でした。
RoleをDB上で表現するのか実装上で表現するのか。実装上で表現するにはどう表現するのかについては明確な答えではないですが現状考えつくなかでスッキリするのは上記の方法かなと思いました。
なんだかすぐより良い方法を思いつきそうな気もしますが、思いついた場合はまた記事にしたいと思います。
ここ最近はいろんなことに対して自分なりの根拠や考え方を持てるようになってきました。人の意見に対しても自分の意見をぶつけたうえでよりよい方法を模索できるようになってきたのでチームメンバーとの議論が楽しくなっていい傾向かなと個人的には感じています。

余談

今回紹介したACLLinuxファイルシステムでも使用されている方法です。
今回こうして権限管理について整理していくなかで言われてみればたしかにそうか、、、となって気づく瞬間がありました。

参考

https://knooto.info/software-design-access-control/#top
https://ja.wikipedia.org/wiki/%E3%83%AD%E3%83%BC%E3%83%AB%E3%83%99%E3%83%BC%E3%82%B9%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E5%88%B6%E5%BE%A1
https://kenfdev.hateblo.jp/entry/2020/01/13/115032
https://www.onelogin.com/jp-ja/learn/rbac-vs-abac
https://ja.wikipedia.org/wiki/%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E5%88%B6%E5%BE%A1%E3%83%AA%E3%82%B9%E3%83%88