権限設計とは、「だれが(ロール)」「何に対して(機能)」「何をできるか(操作)」「どの範囲のデータまでか(データ範囲)」を表にまとめ、その表どおりに動くテーブルと判定処理を決めることです。 中心になる成果物は、行に機能と操作、列にロールを並べたロール×権限マトリクスです。この1枚があると、利用部門との確認、実装、テストのすべてで同じ表を見ながら話せます。

この記事では、表計算ソフトに貼れるマトリクスのテンプレート、架空の受注管理システムを題材にした記入例(8機能・36権限・6ロール)、users/roles/permissions/role_permissions のDDL、権限判定SQLをまとめました。SQLは PostgreSQL 16 で実際に流した結果を載せ、マトリクスのCSVから INSERT 文を作る Python も付けています。データ範囲(自分・自部署・全社)の持たせ方と、RBAC・ABAC・ACLの違いも表で整理します。

確認環境:Ubuntu 24.04、PostgreSQL 16.15、Python 3.11.15。

権限設計で決めること早見表

決めること 内容 成果物 この記事の例
ロール 仕事の役割ごとのまとまり。部署名や役職名そのものではなく「何をする人か」で分ける ロール一覧 営業担当、営業マネージャー、出荷担当、経理担当、システム管理者、閲覧者
機能(リソース) 権限をかける対象 機能一覧 顧客、見積、受注、出荷、請求、商品マスタ、売上集計、ユーザー管理
操作 機能に対してできること 操作一覧 参照・登録・更新・削除・承認・出力
権限 機能×操作の1組 権限一覧(permissions) 「受注の承認」「請求の出力」など36件
ロールと権限の対応 どのロールにどの権限を付けるか ロール×権限マトリクス 216マスのうち74マスを許可
データ範囲 許可された操作が届くデータの広さ マトリクスのセルの記号 自(自分の担当)・部(自部署)・全(全社)
利用者とロールの対応 だれにどのロールを付けるか。兼務の扱い 利用者×ロール表(user_roles) 1人に複数ロールを付けられる
判定のルール 複数ロールを持つときの扱い、許可が無いときの扱い 判定仕様 許可の合算。範囲は広いほうを採用。記載が無ければ不許可

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「権限設計の書き方 ロール×権限マトリクスのテンプレートとRBAC例」

RBAC・ABAC・ACLの違い早見表

権限の持たせ方には代表的な3つのモデルがあります。定義は NIST(米国国立標準技術研究所)の用語集に沿っています。

項目 RBAC(ロールベース) ABAC(属性ベース) ACL(アクセス制御リスト)
考え方 許可する操作を、個々の利用者ではなくロールに結びつける 利用者の属性・対象の属性・環境の条件と、それらで書いた方針で許可を決める 対象ごとに、アクセスできる相手とその権利を並べた一覧を持つ
許可の決まり方 利用者 → ロール → 権限 「部署が同じ」「状態が下書き」などの条件式 対象 → (相手、権利)の一覧
設計書の形 ロール×権限マトリクス 方針(ルール)の一覧表 対象ごとの共有設定
向く場面 役割がはっきりしている業務システム 条件が細かく、組み合わせが多い場合 ファイルやフォルダのように、対象ごとに共有相手が違う場合
気をつける点 例外のたびにロールを増やすと数が膨らむ ルールが増えると「だれが何をできるか」を一覧しにくい 対象が増えると全体の見直しに手間がかかる
この記事での使い方 基本の骨組み データ範囲(自分・自部署・全社)の判定に一部取り入れる 扱わない

業務システムでは、RBACを骨組みにして、データ範囲だけ属性(担当者・部署)で絞る形がよく使われます。この記事もその形で進めます。

権限設計の手順

手順 すること ポイント
1 機能を洗い出す 画面単位ではなく「顧客」「受注」など扱う対象でまとめると、画面が増えても表が変わりにくい
2 操作を決める 参照・登録・更新・削除に、承認と出力(CSVや帳票)を加えた6つが基本
3 ロールを洗い出す 業務の流れに沿って「だれが何をするか」から決める
4 マトリクスを埋める 迷ったら空欄(不許可)から始め、必要なものだけ足す
5 データ範囲を決める 許可するマスに「自・部・全」を書く
6 点検する 登録と承認を同じロールが持っていないか、だれも持たない権限が無いかを見る
7 テーブルと判定処理に落とす マトリクスのCSVからデータを作ると、設計書と実装がずれない

ロール×権限マトリクスのテンプレート

行に「機能×操作」、列にロールを並べます。セルに書く記号は次の4つです。

記号 意味 データベースでの値
全 全社のデータに対して許可 ALL
部 自部署のデータに対して許可 DEPT
自 自分が担当のデータに対して許可 SELF
空欄 許可しない 行を作らない

「○/×」ではなく範囲の記号を書くと、許可するかどうかとデータ範囲を1枚で表せます。データ範囲の区別が要らない機能(マスタなど)は「全」と書きます。

空のテンプレート(CSV)です。表計算ソフトで開き、機能の行とロールの列を増やして使います。

機能,機能コード,操作,操作コード,role_1,role_2,role_3
機能A,func_a,参照,read,,,
機能A,func_a,登録,create,,,
機能A,func_a,更新,update,,,
機能A,func_a,削除,delete,,,
機能A,func_a,承認,approve,,,
機能A,func_a,出力,export,,,
機能B,func_b,参照,read,,,
機能B,func_b,登録,create,,,
機能B,func_b,更新,update,,,
機能B,func_b,削除,delete,,,
機能B,func_b,承認,approve,,,
機能B,func_b,出力,export,,,

記入例:受注管理システムの8機能×6ロール

架空の受注管理システムで埋めた例です。空欄(不許可)は「-」と表示しています。

機能 操作 営業担当 営業マネージャー 出荷担当 経理担当 システム管理者 閲覧者
顧客 参照 全 全 全 全 全 全
顧客 登録 全 全 - - - -
顧客 更新 自 部 - - - -
顧客 削除 - 部 - - - -
顧客 出力 自 部 - - - -
見積 参照 部 部 - - - 全
見積 登録 自 自 - - - -
見積 更新 自 部 - - - -
見積 削除 自 部 - - - -
見積 承認 - 部 - - - -
見積 出力 自 部 - - - -
受注 参照 部 部 全 全 - 全
受注 登録 自 自 - - - -
受注 更新 自 部 - - - -
受注 削除 - 部 - - - -
受注 承認 - 部 - - - -
受注 出力 自 部 - 全 - -
出荷 参照 部 部 全 全 - 全
出荷 登録 - - 全 - - -
出荷 更新 - - 全 - - -
出荷 出力 - - 全 - - -
請求 参照 自 部 - 全 - 全
請求 登録 - - - 全 - -
請求 更新 - - - 全 - -
請求 承認 - - - 全 - -
請求 出力 - - - 全 - -
商品マスタ 参照 全 全 全 全 全 全
商品マスタ 登録 - - - - 全 -
商品マスタ 更新 - - - - 全 -
商品マスタ 削除 - - - - 全 -
売上集計 参照 自 部 - 全 - 全
売上集計 出力 - 部 - 全 - -
ユーザー管理 参照 - - - - 全 -
ユーザー管理 登録 - - - - 全 -
ユーザー管理 更新 - - - - 全 -
ユーザー管理 削除 - - - - 全 -

読み取れる方針は次のとおりです。

ロール 方針
営業担当 自部署の見積・受注を参照でき、登録・更新・出力は自分の担当分だけ
営業マネージャー 自部署の見積・受注を更新・承認できる。削除もマネージャーに限る
出荷担当 全社の受注を参照し、出荷の登録・更新・出力を行う
経理担当 請求の全操作と、全社の売上集計を扱う
システム管理者 商品マスタとユーザー管理を扱う。業務データは顧客の参照だけ
閲覧者 業務データを全社の範囲で参照だけできる

CSV(後半のPythonとSQLでそのまま使います)

matrix.csv という名前で保存します。列名のロールは、roles テーブルのコードと同じ英字にしています。

機能,機能コード,操作,操作コード,sales,sales_mgr,shipping,accounting,admin,viewer
顧客,customer,参照,read,全,全,全,全,全,全
顧客,customer,登録,create,全,全,,,,
顧客,customer,更新,update,自,部,,,,
顧客,customer,削除,delete,,部,,,,
顧客,customer,出力,export,自,部,,,,
見積,quote,参照,read,部,部,,,,全
見積,quote,登録,create,自,自,,,,
見積,quote,更新,update,自,部,,,,
見積,quote,削除,delete,自,部,,,,
見積,quote,承認,approve,,部,,,,
見積,quote,出力,export,自,部,,,,
受注,order,参照,read,部,部,全,全,,全
受注,order,登録,create,自,自,,,,
受注,order,更新,update,自,部,,,,
受注,order,削除,delete,,部,,,,
受注,order,承認,approve,,部,,,,
受注,order,出力,export,自,部,,全,,
出荷,shipment,参照,read,部,部,全,全,,全
出荷,shipment,登録,create,,,全,,,
出荷,shipment,更新,update,,,全,,,
出荷,shipment,出力,export,,,全,,,
請求,invoice,参照,read,自,部,,全,,全
請求,invoice,登録,create,,,,全,,
請求,invoice,更新,update,,,,全,,
請求,invoice,承認,approve,,,,全,,
請求,invoice,出力,export,,,,全,,
商品マスタ,product,参照,read,全,全,全,全,全,全
商品マスタ,product,登録,create,,,,,全,
商品マスタ,product,更新,update,,,,,全,
商品マスタ,product,削除,delete,,,,,全,
売上集計,sales_report,参照,read,自,部,,全,,全
売上集計,sales_report,出力,export,,部,,全,,
ユーザー管理,user_admin,参照,read,,,,,全,
ユーザー管理,user_admin,登録,create,,,,,全,
ユーザー管理,user_admin,更新,update,,,,,全,
ユーザー管理,user_admin,削除,delete,,,,,全,

ロール数×権限数の組み合わせを計算する

マトリクスの大きさと、ロールを使う効果を数えてみます。

"""マトリクスの大きさと、ロールを使う場合・使わない場合の設定件数を比べる。"""
import csv

FIXED = ["機能", "機能コード", "操作", "操作コード"]
with open("matrix.csv", encoding="utf-8-sig", newline="") as f:
    reader = csv.DictReader(f)
    roles = [c for c in reader.fieldnames if c not in FIXED]
    rows = list(reader)

functions = len({r["機能コード"] for r in rows})
actions = len({r["操作コード"] for r in rows})
permissions = len(rows)
cells = len(roles) * permissions
filled = sum(1 for r in rows for role in roles if r[role].strip())

print(f"機能 {functions} × 操作 {actions} = 最大 {functions * actions} 権限(対象外を除いて {permissions} 権限)")
print(f"ロール {len(roles)} × 権限 {permissions} = {cells} マス(うち許可 {filled} マス、{filled / cells:.0%})")

users = 120
print(f"利用者 {users} 人に権限を直接付ける場合: {users} × {permissions} = {users * permissions} マス")
print(f"ロールを使う場合: {cells} マス + 利用者とロールの対応 {users} 件 = {cells + users} 件")
print(f"利用者が1人増えたときの設定: 直接 {permissions} マス / ロール 1 件")
$ python3 count_cells.py
機能 8 × 操作 6 = 最大 48 権限(対象外を除いて 36 権限)
ロール 6 × 権限 36 = 216 マス(うち許可 74 マス、34%)
利用者 120 人に権限を直接付ける場合: 120 × 36 = 4320 マス
ロールを使う場合: 216 マス + 利用者とロールの対応 120 件 = 336 件
利用者が1人増えたときの設定: 直接 36 マス / ロール 1 件

利用者120人に権限を直接付けると4,320マスを管理することになりますが、ロールを挟むと216マス+120件で済みます。人の異動や入れ替わりは「利用者とロールの対応」を1件変えるだけです。逆に、ロールを増やすとマスは「ロール数×権限数」で増えます。ロールが20になれば720マスです。例外のためにロールを足す前に、既存のロールの組み合わせ(兼務)で表せないかを考えると、表を小さく保てます。

users/roles/permissions/role_permissions のDDL

マトリクスを入れるテーブルです。PostgreSQL 16 で実行しました。

CREATE TABLE departments (
    id   integer PRIMARY KEY,
    name varchar(50) NOT NULL
);

CREATE TABLE users (
    id        integer PRIMARY KEY,
    login_id  varchar(50)  NOT NULL UNIQUE,
    name      varchar(100) NOT NULL,
    dept_id   integer      NOT NULL REFERENCES departments (id),
    is_active boolean      NOT NULL DEFAULT true
);

CREATE TABLE roles (
    id   integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    code varchar(30)  NOT NULL UNIQUE,
    name varchar(100) NOT NULL
);

CREATE TABLE permissions (
    id       integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    resource varchar(30)  NOT NULL,   -- 機能コード(order など)
    action   varchar(20)  NOT NULL,   -- 操作コード(read など)
    name     varchar(100) NOT NULL,
    CONSTRAINT uq_permissions UNIQUE (resource, action),
    CONSTRAINT ck_permissions_action
        CHECK (action IN ('read', 'create', 'update', 'delete', 'approve', 'export'))
);

CREATE TABLE user_roles (
    user_id integer NOT NULL REFERENCES users (id),
    role_id integer NOT NULL REFERENCES roles (id),
    PRIMARY KEY (user_id, role_id)
);

CREATE TABLE role_permissions (
    role_id       integer NOT NULL REFERENCES roles (id),
    permission_id integer NOT NULL REFERENCES permissions (id),
    scope         varchar(4) NOT NULL DEFAULT 'ALL',
    PRIMARY KEY (role_id, permission_id),
    CONSTRAINT ck_role_permissions_scope CHECK (scope IN ('SELF', 'DEPT', 'ALL'))
);

-- データ範囲の確認に使う業務テーブル
CREATE TABLE orders (
    id       integer PRIMARY KEY,
    order_no varchar(20)  NOT NULL UNIQUE,
    customer varchar(100) NOT NULL,
    owner_id integer NOT NULL REFERENCES users (id),        -- 担当者
    dept_id  integer NOT NULL REFERENCES departments (id),  -- 担当部署
    amount   integer NOT NULL
);
テーブル 役割 マトリクスとの対応
users 利用者。所属部署を持つ -
roles ロール 列見出し
permissions 権限(機能×操作) 行見出し
role_permissions ロールと権限の対応。データ範囲(scope)を持つ セル(記号が入っているマスだけ行を作る)
user_roles 利用者とロールの対応 -
orders 業務データ。担当者と担当部署を持つ データ範囲の判定に使う

業務テーブルに「担当者(owner_id)」と「担当部署(dept_id)」の列を持たせておくことが、データ範囲を判定する前提になります。

部署、利用者、ロール、利用者とロールの対応、確認用の受注データを入れます。

INSERT INTO departments (id, name) VALUES
    (10, '営業一課'), (20, '営業二課'), (30, '出荷課'), (40, '経理課'), (90, '情報システム課');

INSERT INTO users (id, login_id, name, dept_id) VALUES
    (1, 'sato',     '佐藤', 10),
    (2, 'suzuki',   '鈴木', 10),
    (3, 'takahashi','高橋', 20),
    (4, 'tanaka',   '田中', 30),
    (5, 'ito',      '伊藤', 40),
    (6, 'watanabe', '渡辺', 90);

INSERT INTO roles (code, name) VALUES
    ('sales',      '営業担当'),
    ('sales_mgr',  '営業マネージャー'),
    ('shipping',   '出荷担当'),
    ('accounting', '経理担当'),
    ('admin',      'システム管理者'),
    ('viewer',     '閲覧者');

-- 鈴木さんは営業担当と営業マネージャーを兼ねる
INSERT INTO user_roles (user_id, role_id)
SELECT u.id, r.id
FROM (VALUES ('sato', 'sales'), ('suzuki', 'sales'), ('suzuki', 'sales_mgr'),
             ('takahashi', 'sales'), ('tanaka', 'shipping'),
             ('ito', 'accounting'), ('watanabe', 'admin')) AS v (login_id, role_code)
JOIN users u ON u.login_id = v.login_id
JOIN roles r ON r.code = v.role_code;

INSERT INTO orders (id, order_no, customer, owner_id, dept_id, amount) VALUES
    (1, 'SO-0001', 'あおぞら商事',   1, 10, 120000),
    (2, 'SO-0002', 'みどり工業',     2, 10,  80000),
    (3, 'SO-0003', 'ひまわり物産',   1, 10, 250000),
    (4, 'SO-0004', 'こもれび製作所', 3, 20,  60000);

マトリクスのCSVからINSERT文を作るPython

マトリクスを手で INSERT 文に書き写すと、写し間違いが起こりやすくなります。CSVから機械的に作ります。

"""ロール×権限マトリクス(CSV)から permissions と role_permissions の INSERT 文を作る。
使い方: python3 matrix_to_sql.py matrix.csv > seed_permissions.sql
"""
import csv
import sys

SCOPE = {"自": "SELF", "部": "DEPT", "全": "ALL"}
FIXED = ["機能", "機能コード", "操作", "操作コード"]


def q(text):
    """SQL の文字列リテラルにする(' は '' に置き換える)"""
    return "'" + text.replace("'", "''") + "'"


def main(path):
    with open(path, encoding="utf-8-sig", newline="") as f:
        reader = csv.DictReader(f)
        roles = [c for c in reader.fieldnames if c not in FIXED]
        rows = list(reader)

    out = ["BEGIN;"]
    grants = 0
    for r in rows:
        name = f"{r['機能']}の{r['操作']}"
        out.append(
            "INSERT INTO permissions (resource, action, name) VALUES "
            f"({q(r['機能コード'])}, {q(r['操作コード'])}, {q(name)});"
        )
    for r in rows:
        for role in roles:
            mark = r[role].strip()
            if mark == "":
                continue
            if mark not in SCOPE:
                sys.exit(f"{r['機能']}/{r['操作']}/{role}: 記号 {mark!r} は使えません")
            out.append(
                "INSERT INTO role_permissions (role_id, permission_id, scope) "
                f"SELECT r.id, p.id, {q(SCOPE[mark])} FROM roles r, permissions p "
                f"WHERE r.code = {q(role)} AND p.resource = {q(r['機能コード'])} "
                f"AND p.action = {q(r['操作コード'])};"
            )
            grants += 1
    out.append("COMMIT;")
    print("\n".join(out))
    print(f"permissions {len(rows)} 件, role_permissions {grants} 件", file=sys.stderr)


if __name__ == "__main__":
    main(sys.argv[1])
$ python3 matrix_to_sql.py matrix.csv > seed_permissions.sql
permissions 36 件, role_permissions 74 件

作られたSQLの一部です(全112行)。

BEGIN;
INSERT INTO permissions (resource, action, name) VALUES ('customer', 'read', '顧客の参照');
INSERT INTO permissions (resource, action, name) VALUES ('customer', 'create', '顧客の登録');
…(省略)…
INSERT INTO role_permissions (role_id, permission_id, scope) SELECT r.id, p.id, 'SELF' FROM roles r, permissions p WHERE r.code = 'sales' AND p.resource = 'order' AND p.action = 'export';
INSERT INTO role_permissions (role_id, permission_id, scope) SELECT r.id, p.id, 'DEPT' FROM roles r, permissions p WHERE r.code = 'sales_mgr' AND p.resource = 'order' AND p.action = 'export';
…(省略)…
COMMIT;

IDの数字を直接書かず、ロールのコードと「機能コード+操作コード」で引き当てているので、自動採番のIDが環境ごとに違っても同じSQLを使えます。全体を1つのトランザクションにしているため、途中でエラーになったときは1件も入りません。

流し込んだあと、ロールごとの件数を数えてマトリクスと突き合わせます。

SELECT r.code AS role, count(*) AS perms,
       count(*) FILTER (WHERE rp.scope = 'SELF') AS self,
       count(*) FILTER (WHERE rp.scope = 'DEPT') AS dept,
       count(*) FILTER (WHERE rp.scope = 'ALL')  AS "all"
FROM roles r JOIN role_permissions rp ON rp.role_id = r.id
GROUP BY r.id, r.code ORDER BY r.id;
    role    | perms | self | dept | all 
------------+-------+------+------+-----
 sales      |    17 |   11 |    3 |   3
 sales_mgr  |    22 |    2 |   17 |   3
 shipping   |     7 |    0 |    0 |   7
 accounting |    12 |    0 |    0 |  12
 admin      |     9 |    0 |    0 |   9
 viewer     |     7 |    0 |    0 |   7
(6 rows)

合計は 17+22+7+12+9+7=74 で、Pythonが数えた許可マスの数と一致しています。

権限判定SQL

ある利用者が、ある操作をできるか

「佐藤さん(営業担当)は受注を承認できるか」を調べます。利用者 → ロール → 権限とたどり、1行でもあれば許可です。

SELECT EXISTS (
    SELECT 1
    FROM users u
    JOIN user_roles ur       ON ur.user_id = u.id
    JOIN role_permissions rp ON rp.role_id = ur.role_id
    JOIN permissions p       ON p.id = rp.permission_id
    WHERE u.login_id = 'sato' AND u.is_active
      AND p.resource = 'order' AND p.action = 'approve'
) AS allowed;
 allowed 
---------
 f
(1 row)

login_id を、営業担当と営業マネージャーを兼ねる 'suzuki' に変えると結果が変わります。

 allowed 
---------
 t
(1 row)

どのロールにも記載が無ければ不許可(f)になるのがこの方式の基本です。「許可しない」という行は持ちません。u.is_active を条件に入れているので、利用を止めた人はロールが残っていても不許可になります。

複数ロールを持つ人の権限とデータ範囲

鈴木さんの受注に関する権限を一覧にします。同じ権限を2つのロールから得ている場合は、範囲の広いほうを採用します(ALL>DEPT>SELF)。

SELECT p.resource, p.action,
       CASE max(CASE rp.scope WHEN 'ALL' THEN 3 WHEN 'DEPT' THEN 2 ELSE 1 END)
            WHEN 3 THEN 'ALL' WHEN 2 THEN 'DEPT' ELSE 'SELF' END AS scope,
       string_agg(r.code, ', ' ORDER BY r.id) AS via_roles
FROM users u
JOIN user_roles ur       ON ur.user_id = u.id
JOIN roles r             ON r.id = ur.role_id
JOIN role_permissions rp ON rp.role_id = r.id
JOIN permissions p       ON p.id = rp.permission_id
WHERE u.login_id = 'suzuki' AND p.resource = 'order'
GROUP BY p.id, p.resource, p.action
ORDER BY p.id;
 resource | action  | scope |    via_roles     
----------+---------+-------+------------------
 order    | read    | DEPT  | sales, sales_mgr
 order    | create  | SELF  | sales, sales_mgr
 order    | update  | DEPT  | sales, sales_mgr
 order    | delete  | DEPT  | sales_mgr
 order    | approve | DEPT  | sales_mgr
 order    | export  | DEPT  | sales, sales_mgr
(6 rows)

更新(update)は、営業担当としては「自」、営業マネージャーとしては「部」なので、広いほうの DEPT になっています。via_roles の列で、どのロールからその権限を得ているかがわかるので、問い合わせの調査にも使えます。

判定を関数にまとめる

同じ結合を何度も書かずに済むよう、範囲を返す関数にします。許可が無ければ NULL が返ります。

CREATE FUNCTION permission_scope(p_login text, p_resource text, p_action text)
RETURNS text LANGUAGE sql STABLE AS $$
    SELECT CASE max(CASE rp.scope WHEN 'ALL' THEN 3 WHEN 'DEPT' THEN 2 ELSE 1 END)
               WHEN 3 THEN 'ALL' WHEN 2 THEN 'DEPT' WHEN 1 THEN 'SELF' END
    FROM users u
    JOIN user_roles ur       ON ur.user_id = u.id
    JOIN role_permissions rp ON rp.role_id = ur.role_id
    JOIN permissions p       ON p.id = rp.permission_id
    WHERE u.login_id = p_login AND u.is_active
      AND p.resource = p_resource AND p.action = p_action
$$;

SELECT login_id,
       permission_scope(login_id, 'order', 'read')    AS order_read,
       permission_scope(login_id, 'order', 'update')  AS order_update,
       permission_scope(login_id, 'order', 'approve') AS order_approve
FROM users ORDER BY id;
 login_id  | order_read | order_update | order_approve 
-----------+------------+--------------+---------------
 sato      | DEPT       | SELF         | 
 suzuki    | DEPT       | DEPT         | DEPT
 takahashi | DEPT       | SELF         | 
 tanaka    | ALL        |              | 
 ito       | ALL        |              | 
 watanabe  |            |              | 
(6 rows)

空欄は NULL、つまり不許可です。システム管理者の渡辺さんは、受注に関する権限を持っていません。

データ範囲(自分・自部署・全社)の持たせ方

データ範囲の持たせ方には、主に3つの方法があります。

方法 持ち方 利点 気をつける点
role_permissions に範囲の列を持つ scope 列に SELF/DEPT/ALL マトリクスのセルとそのまま対応する。権限の件数が増えない 範囲の種類を増やすと判定の CASE も増える
権限を範囲ごとに分ける order:read:self、order:read:dept のように別の権限にする 判定が「権限を持つか」だけで済む 権限の件数が範囲の数だけ増える(36件が最大108件)
ロールを範囲ごとに分ける 「営業担当(自部署)」「営業担当(全社)」 テーブルを変えずに始められる ロールの数が増え、マトリクスの列が膨らむ

この記事では1つ目の方法を使っています。一覧を取るSQLでは、範囲に応じて WHERE の条件を切り替えます。

PREPARE visible_orders (text) AS
SELECT o.order_no, o.customer, o.owner_id, o.dept_id
FROM orders o
JOIN users me ON me.login_id = $1
WHERE CASE permission_scope($1, 'order', 'read')
          WHEN 'ALL'  THEN true
          WHEN 'DEPT' THEN o.dept_id  = me.dept_id
          WHEN 'SELF' THEN o.owner_id = me.id
          ELSE false
      END
ORDER BY o.id;

EXECUTE visible_orders('sato');       -- 営業一課の営業担当(参照は自部署)
EXECUTE visible_orders('takahashi');  -- 営業二課の営業担当(参照は自部署)
EXECUTE visible_orders('tanaka');     -- 出荷担当(参照は全社)
EXECUTE visible_orders('watanabe');   -- システム管理者(受注の権限なし)
 order_no |   customer   | owner_id | dept_id 
----------+--------------+----------+---------
 SO-0001  | あおぞら商事 |        1 |      10
 SO-0002  | みどり工業   |        2 |      10
 SO-0003  | ひまわり物産 |        1 |      10
(3 rows)

 order_no |    customer    | owner_id | dept_id 
----------+----------------+----------+---------
 SO-0004  | こもれび製作所 |        3 |      20
(1 row)

 order_no |    customer    | owner_id | dept_id 
----------+----------------+----------+---------
 SO-0001  | あおぞら商事   |        1 |      10
 SO-0002  | みどり工業     |        2 |      10
 SO-0003  | ひまわり物産   |        1 |      10
 SO-0004  | こもれび製作所 |        3 |      20
(4 rows)

 order_no | customer | owner_id | dept_id 
----------+----------+----------+---------
(0 rows)

同じSQLで、利用者ごとに見える行が変わりました。ELSE false があるので、権限が無い人には1行も返りません。

操作を 'update' に変えると、更新できる行だけが取れます。佐藤さん(更新は自分の担当分)は2件、鈴木さん(更新は自部署)は3件です。

 order_no | owner_id 
----------+----------
 SO-0001  |        1
 SO-0003  |        1
(2 rows)

 order_no | owner_id 
----------+----------
 SO-0001  |        1
 SO-0002  |        2
 SO-0003  |        1
(3 rows)

一覧に出すときだけでなく、更新や削除を実行するSQLにも同じ範囲の条件を入れることが大切です。画面でボタンを隠すだけでは、URLやAPIを直接呼ばれたときに防げないためです。

マトリクスをSQLで点検する

データになったマトリクスは、SQLで点検できます。

登録と承認を同じロールが持っていないか

申請する人と承認する人を分けたい機能について、両方を持つロールを探します。

SELECT r.name AS role, c.resource,
       rc.scope AS create_scope, ra.scope AS approve_scope
FROM roles r
JOIN role_permissions rc ON rc.role_id = r.id
JOIN permissions c       ON c.id = rc.permission_id AND c.action = 'create'
JOIN role_permissions ra ON ra.role_id = r.id
JOIN permissions a       ON a.id = ra.permission_id AND a.action = 'approve'
                        AND a.resource = c.resource
ORDER BY r.id, c.resource;
       role       | resource | create_scope | approve_scope 
------------------+----------+--------------+---------------
 営業マネージャー | order    | SELF         | DEPT
 営業マネージャー | quote    | SELF         | DEPT
 経理担当         | invoice  | ALL          | ALL
(3 rows)

3件見つかりました。対応は2通りあります。

対応 内容 例
ロールを分ける 承認だけを持つロールを作る 「経理マネージャー」を追加し、請求の承認を移す
判定に条件を足す 「自分が登録したデータは承認できない」をプログラム側の判定に入れる 営業マネージャーが自分で登録した受注は、別のマネージャーが承認する

後者は「登録者と承認者が別人であること」というデータの属性による条件で、RBACにABACの考え方を少し足す形です。マトリクスの欄外に注記として書いておきます。

1つのロールしか持たない権限

担当が1つのロールに限られる権限を洗い出します。意図どおりかを確かめる材料になります。

SELECT p.resource, p.action, count(rp.role_id) AS roles
FROM permissions p
LEFT JOIN role_permissions rp ON rp.permission_id = p.id
GROUP BY p.id, p.resource, p.action
HAVING count(rp.role_id) <= 1
ORDER BY p.id;
  resource  | action  | roles 
------------+---------+-------
 customer   | delete  |     1
 quote      | approve |     1
 order      | delete  |     1
 order      | approve |     1
…(省略)…
 user_admin | update  |     1
 user_admin | delete  |     1
(18 rows)

roles が 0 の行があれば、だれも実行できない権限です。今回は 0 の行はありませんでした。

制約が止めてくれる入力

テーブルの制約があると、規則に合わないデータは登録時に止まります。

INSERT INTO role_permissions (role_id, permission_id, scope) VALUES (1, 1, 'ALL');
INSERT INTO role_permissions (role_id, permission_id, scope) VALUES (6, 2, 'TEAM');
INSERT INTO permissions (resource, action, name) VALUES ('order', 'print', '受注の印刷');
ERROR:  duplicate key value violates unique constraint "role_permissions_pkey"
DETAIL:  Key (role_id, permission_id)=(1, 1) already exists.
ERROR:  new row for relation "role_permissions" violates check constraint "ck_role_permissions_scope"
DETAIL:  Failing row contains (6, 2, TEAM).
ERROR:  new row for relation "permissions" violates check constraint "ck_permissions_action"
DETAIL:  Failing row contains (37, order, print, 受注の印刷).

同じマスの二重登録、決めていない範囲、決めていない操作がそれぞれ止まりました。

よくあるつまずきと整え方

起きること 理由 整え方
ロールが部署や役職の数だけ増える 組織図をそのままロールにしている 「何をする人か」で分け、部署の違いはデータ範囲で表す
例外のたびにロールが増える 1人だけの特例をロールにしている 既存ロールの兼務で表せないかを先に考える
画面ごとに権限を作り、数が膨らむ 権限の単位が細かすぎる 機能(扱う対象)×操作でまとめる
プログラムにロール名の判定が散らばる 「営業マネージャーなら」と書いている 「受注の承認ができるなら」と権限で判定する
画面では隠れているのに操作できてしまう 表示の制御だけで、サーバー側の判定が無い 更新・削除のSQLにも範囲の条件を入れる
設計書と実装がずれる マトリクスを手で写している CSVからデータを作り、件数を突き合わせる
出力(CSV・帳票)が参照と同じ扱いになっている まとめて持ち出せる操作を区別していない 出力を独立した操作にして、ロールを絞る

4つ目は特に効果があります。判定を権限でそろえておくと、ロールの追加や組み替えはデータの変更だけで済み、プログラムを直さずに対応できます。

よくある質問

Q. ロールはいくつくらいが適切ですか? A. 決まった数はありませんが、マトリクスを1枚で見渡せる数に収めると確認しやすくなります。この記事の例は6ロールです。似た列(許可のマスがほとんど同じロール)が並んだら、1つにまとめてデータ範囲で差を付けられないかを見直します。

Q. 1人に複数のロールを付けてもよいですか? A. 付けられる設計にしておくと、兼務を新しいロールなしで表せます。その場合は「許可は合算、範囲は広いほうを採用」のように、重なったときの扱いを判定仕様に書いておきます。

Q. 「不許可」を明示する行(拒否)は必要ですか? A. 「記載が無ければ不許可」とするだけで足りることが多く、表も判定も単純になります。拒否の行を持つと「許可と拒否が重なったらどちらを優先するか」を決める必要が出てきます。

Q. マトリクスの行は画面単位と機能単位のどちらがよいですか? A. 機能(顧客・受注などの扱う対象)単位がおすすめです。同じ受注データを扱う画面が増えても、権限の行は増えません。画面やAPIとの対応は、別の「画面×権限」の表にまとめます。

まとめ

  • 権限設計は「ロール・機能・操作・データ範囲」の4つを決め、ロール×権限マトリクスにまとめる
  • セルに「全・部・自」を書くと、許可するかどうかとデータ範囲を1枚で表せる
  • ロールを挟むと、設定は「ロール数×権限数+利用者数」で済む(例では4,320マスが336件)
  • テーブルは users/roles/permissions/role_permissions/user_roles の5つが基本。範囲は role_permissions の列で持てる
  • 判定は「記載が無ければ不許可」、複数ロールは許可の合算で範囲は広いほうと決めておく
  • マトリクスのCSVからINSERT文を作り、件数を突き合わせると、設計書と実装がずれない
  • 登録と承認の兼ね持ちは、SQLで洗い出してロールを分けるか判定に条件を足す

参考・出典

  • NIST Computer Security Resource Center Glossary「role-based access control」 https://csrc.nist.gov/glossary/term/role_based_access_control
  • NIST Computer Security Resource Center Glossary「attribute based access control」 https://csrc.nist.gov/glossary/term/attribute_based_access_control
  • NIST Computer Security Resource Center Glossary「access control list」 https://csrc.nist.gov/glossary/term/access_control_list

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「権限設計の書き方 ロール×権限マトリクスのテンプレートとRBAC例」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?