snippetsqlMinor
How to implement business logic permissions in PostgreSQL (or SQL in general)?
Viewed 0 times
postgresqlpermissionsbusinessimplementsqllogicgeneralhow
Problem
Let's assume I have a table of items:
Now, I want to introduce the concept of "permissions" for each item (please note, I'm not talking about database access permissions here, but business logic permissions for that item). Each item has default permissions and also per-user permissions that may override default permissions.
I tried to think of several ways to implement this and came up with the following solutions, but I'm not sure on which is the best and why:
1) The Boolean Solution
Use a boolean column for each permission:
Advantages: Each permission is named.
Disadvantages: There are dozens of permissions which increases the number of columns significantly and you have to define them twice (once in each table).
2) The Integer Solution
Use an integer and treat it as a bitfield (i.e. bit 0 is for
Advantages: very fast.
Disadvantages: You have to keep track of which bit stands for which permission in both the database and the front-end interface.
3) The Bitfield Solution
Same as 2), but use
4) The Enum Solution
Use an enum type for the permissions:
```
CREATE TYPE permission AS ENUM ('can_change_description', 'can_change_price', .
CREATE TABLE items
(
item serial PRIMARY KEY,
...
);Now, I want to introduce the concept of "permissions" for each item (please note, I'm not talking about database access permissions here, but business logic permissions for that item). Each item has default permissions and also per-user permissions that may override default permissions.
I tried to think of several ways to implement this and came up with the following solutions, but I'm not sure on which is the best and why:
1) The Boolean Solution
Use a boolean column for each permission:
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,
...
);Advantages: Each permission is named.
Disadvantages: There are dozens of permissions which increases the number of columns significantly and you have to define them twice (once in each table).
2) The Integer Solution
Use an integer and treat it as a bitfield (i.e. bit 0 is for
can_change_description, bit 1 is for can_change_price, and so on, and use bitwise operations to set or read permissions).CREATE DOMAIN permissions AS integer;Advantages: very fast.
Disadvantages: You have to keep track of which bit stands for which permission in both the database and the front-end interface.
3) The Bitfield Solution
Same as 2), but use
bit(n). Most likely the same advantages and disadvantages, maybe slightly slower.4) The Enum Solution
Use an enum type for the permissions:
```
CREATE TYPE permission AS ENUM ('can_change_description', 'can_change_price', .
Solution
I know that you are not asking about database security per se, but you can do what you want using database security. You can even use this in a web app. If you don't want to use database security, then the schemas still apply.
You want column-level security, row-level security, and probably hierarchical role management. Role-Based security is much easier to manage than User-Based security.
This example code is for PostgreSQL 9.4, which comes out soon. You can do it with 9.3, but there is more manual labour required.
You want everything to be indexable if you are concerned with performance †, which you should be. This means that bit-mask and array fields probably won't be a good idea.
In this example, we keep the main data tables in the
Put a trigger on data.thing for inserts and updates enforcing that the owner column is the current_user. Perhaps allow only the owner to delete his own records (another trigger).
Create a
Next, create an access-control list table:
Change your view to account for ACLs:
Create a default row privileges table:
Put a trigger on insert on data.thing so that it copies default row privileges to security.thing_acl .
† In this case pg_has_role is probably not indexable. You would have to get a list of all superior roles to current_user and compare to the owner/grantee value instead.
You want column-level security, row-level security, and probably hierarchical role management. Role-Based security is much easier to manage than User-Based security.
This example code is for PostgreSQL 9.4, which comes out soon. You can do it with 9.3, but there is more manual labour required.
You want everything to be indexable if you are concerned with performance †, which you should be. This means that bit-mask and array fields probably won't be a good idea.
In this example, we keep the main data tables in the
data schema, and corresponding views in 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
);Put a trigger on data.thing for inserts and updates enforcing that the owner column is the current_user. Perhaps allow only the owner to delete his own records (another trigger).
Create a
WITH CHECK OPTION view, which is what users will actually use. Try really hard to make it updatable, otherwise you'll need triggers/rules, which is more work. 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;Next, create an access-control list table:
--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
);Change your view to account for ACLs:
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 a default row privileges table:
create table security.default_row_privileges (
table_name name,
role_name name,
privilege char(1),
primary key (table_name, role_name, privilege)
);Put a trigger on insert on data.thing so that it copies default row privileges to security.thing_acl .
- Adjust table-level security appropriately (prevent inserts from unwanted users). No one should be able to read the data or security schemas.
- Adjust column-level security appropriately (prevent some users from seeing/editing some columns). You can use has_column_privilege() to check that a user can see a column.
- Probably want security definer tag on your view.
- Consider adding
grantorandadmin_optioncolumns to acl tables to track who granted the privilege, and whether the grantee can manage privileges on that row.
- Test lots
† In this case pg_has_role is probably not indexable. You would have to get a list of all superior roles to current_user and compare to the owner/grantee value instead.
Code Snippets
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
);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
);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)
);Context
StackExchange Database Administrators Q#62615, answer score: 9
Revisions (0)
No revisions yet.