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


16

假设我有一个项目表:

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)整数解决方案

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

CREATE DOMAIN permissions AS integer;

优点:非常快。

缺点:您必须跟踪数据库和前端接口中哪个位代表哪个权限。

3)位域解决方案

与2)相同,但使用bit(n)。优点和缺点可能相同,但速度可能稍慢。

4)枚举解决方案

对权限使用枚举类型:

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)    
);

优点:易于命名个人权限(您无需处理位)。

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

尤其是因为必须为该项目的每个页面视图都检索默认权限,所以最后一种选择对性能的影响可能很大。

5)枚举数组解决方案

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

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

CREATE TABLE items
(
    item serial PRIMARY KEY,

    granted_permissions permission ARRAY,
    ...
);

优点:易于命名个人权限(您无需处理位)。

缺点:破坏了第一种正常形式,并且有点难看。如果许可数量很大(大约50个),则连续占用大量字节。

您能想到其他选择吗?

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

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


2
具有数十种不同的权限,我可能会选择一个(或多个)bigint字段(每个字段适合64位)或一个位字符串。我在SO上
Erwin Brandstetter 2014年

Answers:


7

我知道您并不是在问数据库安全性本身,而是可以使用数据库安全性来做您想做的事情。您甚至可以在网络应用中使用它。如果您不想使用数据库安全性,那么这些架构仍然适用。

您需要列级安全性,行级安全性以及可能的分层角色管理。与基于用户的安全性相比,基于角色的安全性易于管理。

此示例代码适用于即将推出的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
);

在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;

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

--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()来检查用户是否可以看到列。
  • 可能要在视图上使用安全定义器标签。
  • 考虑添加grantoradmin_option在ACL表中列,以跟踪谁授予了特权,以及被授予者是否可以管理该行的特权。
  • 测试批次

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


您是否看到“ 我在这里不谈论数据库访问权限 ”部分?
a_horse_with_no_name 2014年

@a_horse_with_no_name是的。他可以编写自己的RLS / ACL系统,可以使用数据库的内置安全性来完成他所要求的。
尼尔·麦圭根

感谢您的详细回答!但是,我认为使用数据库角色不是正确的答案,因为不仅员工,而且每个用户都可能具有权限。例如“ can_view_item”,“ can_bulk_order_item”或“ can_review_item”。我认为,我最初选择的权限名称使您相信这仅与员工权限有关,但是所有这些名称只是抽象化复杂性的示例。正如我在原始问题中所说的那样,它是关于每个用户的权限,而不是每个员工的权限。
JohnCand 2014年

无论如何,必须对users表中的每个用户行都具有单独的数据库角色,这似乎是过大的,并且难以管理。但是,我确实认为您的答案对于仅实施人员权限的开发人员很有价值。
JohnCand 2014年

1
@JohnCand我真的看不到在其他地方管理权限的简单性,但是一旦找到它,请把我们指向您的解决方案!:)
Neil McGuigan 2014年

4

您是否考虑过使用访问控制列表 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'

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


1

我可以想到另一种编码方式,即关系编码

如果您不需要该permission_per_item表,则可以跳过该表PermissionsItems直接连接到该item_per_user_permissions表。

在此处输入图片说明

图例

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.