如何在PostgreSQL(或一般的SQL)中实现业务逻辑权限?

Joh*_*and 19 postgresql enum

假设我有一个项目表:

CREATE TABLE items
(
    item serial PRIMARY KEY,
    ...
);
Run Code Online (Sandbox Code Playgroud)

现在,我想为每个项目介绍“权限”的概念(请注意,我在这里不是在谈论数据库访问权限,而是该项目的业务逻辑权限)。每个项目都具有默认权限以及可以覆盖默认权限的每用户权限。

我试图想出几种方法来实现这一点,并提出了以下解决方案,但我不确定哪个是最好的以及为什么:

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,
    ...
);
Run Code Online (Sandbox Code Playgroud)

优点:每个权限都是命名的。

缺点:有几十个权限会显着增加列数,并且您必须定义它们两次(每个表中一次)。

2)整数解

使用整数并将其视为位域(即位 0 表示can_change_description,位 1 表示can_change_price,依此类推,并使用按位运算来设置或读取权限)。

CREATE DOMAIN permissions AS integer;
Run Code Online (Sandbox Code Playgroud)

优点:非常快。

缺点:你必须在数据库和前端界面中跟踪哪个位代表哪个权限。

3)位域解决方案

与 2) 相同,但使用bit(n). 很可能具有相同的优点和缺点,可能稍微慢一些。

4)枚举解决方案

对权限使用枚举类型:

CREATE TYPE permission AS ENUM ('can_change_description', 'can_change_price', .....);
Run Code Online (Sandbox Code Playgroud)

然后为默认权限创建一个额外的表:

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

    PRIMARY KEY(item, perm)
);
Run Code Online (Sandbox Code Playgroud)

并将每用户定义表更改为:

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)    
);
Run Code Online (Sandbox Code Playgroud)

优点:易于命名单个权限(您不必处理位位置)。

缺点:即使只是检索默认权限,也需要访问两个附加表:第一,默认权限表,第二,存储枚举值的系统目录。

尤其是因为必须为该项目的每个页面视图检索默认权限,所以最后一个替代方案的性能影响可能会很大。

5) 枚举数组解决方案

与 4) 相同,但使用一个数组来保存所有(默认)权限:

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

CREATE TABLE items
(
    item serial PRIMARY KEY,

    granted_permissions permission ARRAY,
    ...
);
Run Code Online (Sandbox Code Playgroud)

优点:易于命名单个权限(您不必处理位位置)。

缺点:打破第一范式,有点难看。如果权限数量很大(大约 50),则连续占用相当多的字节。

你能想到其他的选择吗?

应该采取哪种方法,为什么?

请注意:这是之前在 Stackoverflow 上发布问题的修改版本。

Nei*_*gan 8

我知道您并不是在问数据库安全本身,但是您可以使用数据库安全来做您想做的事情。您甚至可以在 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
);
Run Code Online (Sandbox Code Playgroud)

在 data.thing 上放置触发器以进行插入和更新,强制所有者列是 current_user。也许只允许所有者删除他自己的记录(另一个触发器)。

创建一个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;
Run Code Online (Sandbox Code Playgroud)

接下来,创建一个访问控制列表表:

--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
);
Run Code Online (Sandbox Code Playgroud)

更改您的视图以考虑 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;
Run Code Online (Sandbox Code Playgroud)

创建一个默认的行权限表:

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

  primary key (table_name, role_name, privilege)
);
Run Code Online (Sandbox Code Playgroud)

在 data.thing 上插入一个触发器,以便它将默认行权限复制到 security.thing_acl 。

  • 适当调整表级安全性(防止不需要的用户插入)。没有人应该能够读取数据或安全模式。
  • 适当调整列级安全性(防止某些用户查看/编辑某些列)。您可以使用 has_column_privilege() 来检查用户是否可以看到列。
  • 可能需要在您的视图上使用安全定义器标记。
  • 考虑向acl 表添加grantoradmin_option列以跟踪授予权限的人,以及被授予者是否可以管理该行的权限。
  • 测试批次

† 在这种情况下 pg_has_role 可能是不可索引的。您必须获得 current_user 的所有高级角色的列表,并与所有者/被授予者的值进行比较。

  • @JohnCand 我真的不明白在其他地方管理权限如何更容易,但是一旦找到您的解决方案,请向我们指出您的解决方案!:) (2认同)

小智 5

您是否考虑过使用访问控制列表PostgreSQL 扩展?

它包含本机 PostgreSQL 数据类型 ACE 和一组允许您检查用户是否有权访问数据的函数。它适用于 PostgreSQL 角色系统或代表您的应用程序用户/角色 ID 的抽象数字(或 UUID)。

在您的情况下,您只需向数据表中添加一个 ACL 列,并使用其中一个acl_check_access函数根据 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'
Run Code Online (Sandbox Code Playgroud)

使用 ACL 是处理业务逻辑权限的一种极其灵活的方式。此外,它非常快——平均开销仅为读取记录所需时间的 25%。唯一的限制是它支持每个对象类型最多 16 个自定义权限。