web-dev-qa-db-ja.com

PostgreSQL(または一般にSQL)でビジネスロジックのアクセス許可を実装する方法

アイテムのテーブルがあるとしましょう:

CREATE TABLE items
(
    item serial PRIMARY KEY,
    ...
);

ここで、各アイテムの「アクセス許可」の概念を紹介します(注意してください、私はではなく、データベースアクセス許可について話していますが、そのアイテムのビジネスロジックアクセス許可です)。各アイテムにはデフォルトのアクセス許可とデフォルトの権限を上書きする可能性のあるユーザーごとの権限。

私はこれを実装するいくつかの方法を考えてみましたが、次の解決策を考え出しましたが、どちらが最善で、なぜかはわかりません:

1)ブール解

権限ごとにブール列を使用します。

CREATE TABLE items
(
    item serial PRIMARY KEY,

    can_change_description boolean NOT NULL,
    can_change_price boolean NOT NULL,
    can_delete_item_from_store boolean NOT NULL,
    ...
);

CREATE TABLE item_per_user_permissions
(
    item int NOT NULL REFERENCES items(item),
    user int NOT NULL REFERENCES users(user),

    PRIMARY KEY(item, user),

    can_change_description boolean NOT NULL,
    can_change_price boolean NOT NULL,
    can_delete_item_from_store boolean NOT NULL,
    ...
);

利点:各権限に名前が付けられます。

短所:列の数を大幅に増やす多数の権限があり、2回定義する必要があります(各テーブルで1回)。

2)整数解

整数を使用し、それをビットフィールドとして扱います(つまり、ビット0はcan_change_description用で、ビット1はcan_change_price用で、ビット単位の演算を使用して権限を設定または読み取ります)。

CREATE DOMAIN permissions AS integer;

利点:非常に高速。

短所:データベースとフロントエンドインターフェイスの両方で、どのビットがどの権限を表すかを追跡する必要があります。

3)ビットフィールドソリューション

2)と同じですが、bit(n)を使用します。おそらく同じ長所と短所があり、少し遅いかもしれません。

4)Enumソリューション

権限には列挙型を使用します。

CREATE TYPE permission AS ENUM ('can_change_description', 'can_change_price', .....);

次に、デフォルトの権限用に追加のテーブルを作成します。

CREATE TABLE item_default_permissions
(
    item int NOT NULL REFERENCES items(item),
    perm permission NOT NULL,

    PRIMARY KEY(item, perm)
);

ユーザーごとの定義テーブルを次のように変更します。

CREATE TABLE item_per_user_permissions
(
    item int NOT NULL REFERENCES items(item),
    user int NOT NULL REFERENCES users(user),
    perm permission NOT NULL,

    PRIMARY KEY(item, user, perm)    
);

利点:個々の権限に簡単に名前を付けることができます(ビット位置を処理する必要はありません)。

欠点:既定のアクセス許可を取得するだけでも、2つの追加のテーブルにアクセスする必要があります。1つ目は既定のアクセス許可テーブル、もう1つは列挙値を格納するシステムカタログです。

特に、デフォルトのアクセス許可はそのアイテムのすべてのページビューに対して取得する必要があるため、//なので、最後の選択肢のパフォーマンスへの影響は大きくなる可能性があります。

5)列挙型配列ソリューション

4)と同じですが、配列を使用してすべての(デフォルト)権限を保持します。

CREATE TYPE permission AS ENUM ('can_change_description', 'can_change_price', .....);

CREATE TABLE items
(
    item serial PRIMARY KEY,

    granted_permissions permission ARRAY,
    ...
);

利点:個々の権限に簡単に名前を付けることができます(ビット位置を処理する必要はありません)。

短所:1番目の通常形式を壊し、少し見苦しいです。許可の数が多い場合(約50)、連続してかなりのバイト数を占有します。

他の代替案を思いつきますか?

どのアプローチを採用する必要があるのか​​、そしてその理由は?

注意:これは Stackoverflowで以前に投稿された質問 の修正版です。

16
JohnCand

私はあなたがデータベースのセキュリティについて質問していないことを知っていますそれ自体、しかしあなたはデータベースのセキュリティを使用してあなたが望むことをすることができます。これをWebアプリで使用することもできます。データベースのセキュリティを使用しない場合でも、スキーマは適用されます。

列レベルのセキュリティ、行レベルのセキュリティ、そしておそらく階層的なロール管理が必要です。ロールベースのセキュリティは、ユーザーベースのセキュリティよりも管理がはるかに簡単です。

このサンプルコードはPostgreSQL 9.4用で、間もなくリリースされます。 9.3でそれを行うことができますが、より多くの手作業が必要です。

パフォーマンス†に関心がある場合は、すべてをインデックスに登録できるようにしてください。これは、ビットマスクと配列フィールドはおそらく良い考えではないことを意味します。

この例では、主要なデータテーブルをdataスキーマに、対応するビューをpublicに保持しています。

create schema data; --main data tables
create schema security; --acls, security triggers, default privileges

create table data.thing (
  thing_id int primary key,
  subject text not null, --or whatever
  owner name not null
);

所有者列がcurrent_userであることを強制する挿入および更新のトリガーをdata.thingに配置します。おそらく、所有者だけが自分のレコードを削除することを許可します(別のトリガー)。

ユーザーが実際に使用するWITH CHECK OPTIONビューを作成します。それを更新可能にするために本当に一生懸命努力してください。そうしないと、より多くの作業であるトリガー/ルールが必要になります。

create view public.thing with(security_barrier) as 
select
thing_id,
subject,
owner,
from data.thing
where
pg_has_role(owner, 'member') --only owner or roles "above" him can view his rows. 
WITH CHECK OPTION;

次に、アクセス制御リストテーブルを作成します。

--privileges r=read, w=write

create table security.thing_acl (
  thing_id int,
  grantee name, --the role to whom your are granting the privilege
  privilege char(1) check (privilege in ('r','w') ),

  primary key (thing_id, grantee, privilege),

  foreign key (thing_id) references data.thing(thing_id) on delete cascade
);

ACLを考慮してビューを変更します。

drop view public.thing;

create view public.thing with(security_barrier) as 
select
thing_id,
subject,
owner
from data.thing a
where
pg_has_role(owner, 'member')
or exists (select 1 from security.thing_acl b where b.thing_id = a.thing_id and pg_has_role(grantee, 'member') and privilege='r')
with check option;

デフォルトの行特権テーブルを作成します。

create table security.default_row_privileges (
  table_name name,
  role_name name,
  privilege char(1),

  primary key (table_name, role_name, privilege)
);

Data.thingの挿入時にトリガーを設定して、デフォルトの行特権をsecurity.thing_aclにコピーします。

  • テーブルレベルのセキュリティを適切に調整します(不要なユーザーからの挿入を防止します)。データやセキュリティスキーマを読み取ることはできません。
  • 列レベルのセキュリティを適切に調整します(一部のユーザーが一部の列を表示/編集できないようにします)。 has_column_privilege()を使用して、ユーザーが列を表示できることを確認できます。
  • おそらくビューにセキュリティ定義タグが必要です。
  • grantor列とadmin_option列をACLテーブルに追加して、誰が特権を付与したかを追跡し、被付与者がその行の特権を管理できるかどうかを検討してください。
  • テストロット

†この場合、pg_has_roleはおそらくインデックス化できません。 current_userよりも上位のすべてのロールのリストを取得し、代わりに所有者/付与者の値と比較する必要があります。

7
Neil McGuigan

Access Control List PostgreSQL拡張機能の使用を検討しましたか?

これには、ネイティブのPostgreSQLデータ型ACEと、ユーザーがデータにアクセスする権限があるかどうかを確認できる一連の関数が含まれています。 PostgreSQLロールシステムまたはアプリケーションのユーザー/ロールIDを表す抽象番号(またはUUID)のいずれかで機能します。

あなたのケースでは、データテーブルにACL列を追加し、acl_check_access関数の1つを使用して、ACLに対してユーザーをチェックします。

CREATE TABLE items
(
    item serial PRIMARY KEY,
    acl ace[],
    ...
);

INSERT INTO items(acl, ...) VALUES ('{a//<user id>=r, a//<role id>=rwd, ...}');

SELECT * FROM items where acl_check_access(acl, 'r', <roles of the user>, false) = 'r'

ACLの使用は、ビジネスロジックのアクセス許可を処理する非常に柔軟な方法です。さらに、それは信じられないほど高速です—平均オーバーヘッドは、レコードの読み取りに必要な時間の25%にすぎません。唯一の制限は、オブジェクトタイプごとに最大16のカスタム権限をサポートすることです。

4
Slonopotamus

これをエンコードする別の可能性、リレーショナルの可能性を考えることができます

permission_per_itemテーブルが必要ない場合は、スキップして、PermissionsおよびItemsitem_per_user_permissionsテーブルに直接接続できます。

enter image description here

凡例

1
miracle173